@fusion.dev/helix-headless 0.163.0 → 0.165.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/custom-elements.json +1068 -46
- package/dist/callout/root/callout-root.d.ts +43 -4
- package/dist/callout/root/callout-root.js +58 -2
- package/dist/code-block/patch.d.ts +42 -0
- package/dist/code-block/patch.js +118 -0
- package/dist/code-block/root/code-block-root.d.ts +37 -1
- package/dist/code-block/root/code-block-root.js +57 -9
- package/dist/color-picker/root/color-picker-root.d.ts +7 -3
- package/dist/color-picker/root/color-picker-root.js +7 -4
- package/dist/combobox/root/combobox-root.d.ts +1 -0
- package/dist/combobox/root/combobox-root.js +10 -1
- package/dist/data-table/root/data-table-root.d.ts +1 -0
- package/dist/data-table/root/data-table-root.js +1 -0
- package/dist/date-input/root/date-input-root.d.ts +3 -3
- package/dist/date-input/root/date-input-root.js +8 -5
- package/dist/element-events.generated.js +7 -1
- package/dist/format-date/root/format-date-root.d.ts +3 -1
- package/dist/format-date/root/format-date-root.js +4 -1
- package/dist/lightbox/item/lightbox-item.js +3 -1
- package/dist/lightbox/root/lightbox-root.d.ts +28 -0
- package/dist/lightbox/root/lightbox-root.js +21 -2
- package/dist/number-input/root/number-input-root.d.ts +32 -9
- package/dist/number-input/root/number-input-root.js +53 -9
- package/dist/object-inspector/root/object-inspector-root.d.ts +23 -0
- package/dist/object-inspector/root/object-inspector-root.js +19 -1
- package/dist/option/root/index.d.ts +1 -0
- package/dist/option/root/index.js +1 -0
- package/dist/table/grid-controller.d.ts +7 -0
- package/dist/table/grid-controller.js +15 -1
- package/dist/table/root/table-root.d.ts +10 -0
- package/dist/table/root/table-root.js +18 -1
- package/dist/tag/root/index.d.ts +1 -0
- package/dist/tag/root/index.js +1 -0
- package/dist/time-input/root/time-input-root.d.ts +4 -3
- package/dist/time-input/root/time-input-root.js +5 -4
- package/dist/time-picker/root/time-picker-root.d.ts +1 -1
- package/dist/time-picker/root/time-picker-root.js +5 -2
- package/dist/video/root/video-root.js +2 -1
- package/dist/video-playlist/item/video-playlist-item.d.ts +8 -0
- package/dist/video-playlist/item/video-playlist-item.js +7 -0
- package/dist/video-playlist/root/video-playlist-root.d.ts +60 -0
- package/dist/video-playlist/root/video-playlist-root.js +55 -1
- package/lint.json +1 -1
- package/package.json +5 -5
- package/types/jsx.d.ts +388 -40
- package/types/vue.d.ts +95 -13
- package/types/words.d.ts +58 -0
|
@@ -15,8 +15,12 @@ declare const HelixHeadlessCallout_base: (new (...args: any[]) => import("@fusio
|
|
|
15
15
|
* region around it.
|
|
16
16
|
*
|
|
17
17
|
* Set `dismissible` for a note a reader may put away, as Shoelace's `closable`
|
|
18
|
-
* alert does. The button fires `helix-dismiss
|
|
19
|
-
*
|
|
18
|
+
* alert does. The button fires `helix-dismiss`, and unless a listener cancels
|
|
19
|
+
* it the callout hides itself: `open` turns false, `:state(exiting)` holds
|
|
20
|
+
* while its exit animations run, and then it is `hidden`. Remove it in your
|
|
21
|
+
* listener to take it out of the page, or set `open` to show it again, which
|
|
22
|
+
* holds `:state(entering)` while it comes back. Focus on its dismiss button
|
|
23
|
+
* moves to the next callout's, or to the tab stop beside it, once it hides.
|
|
20
24
|
*
|
|
21
25
|
* A callout sets a passage apart inside a document and is read where it sits.
|
|
22
26
|
* For page-level chrome above the masthead that is a live region and can be
|
|
@@ -33,13 +37,19 @@ declare const HelixHeadlessCallout_base: (new (...args: any[]) => import("@fusio
|
|
|
33
37
|
* @slot heading - A heading above the content.
|
|
34
38
|
* @slot icon - Replaces the icon chosen by `tone`.
|
|
35
39
|
*
|
|
36
|
-
* @event {HelixDismissEvent} helix-dismiss - The dismiss button was pressed.
|
|
40
|
+
* @event {HelixDismissEvent} helix-dismiss - The dismiss button was pressed. Unless a listener cancels it, the callout hides itself, and a listener may remove it instead. Cancelable: cancel it to keep the callout showing.
|
|
37
41
|
*
|
|
38
42
|
* @csspart content - The content region.
|
|
39
43
|
* @csspart dismiss - The button that dismisses the callout, shown when `dismissible` is set.
|
|
40
44
|
* @csspart dismiss-mark - The cross on the dismiss button, shown when the `dismiss-icon` slot is empty.
|
|
41
45
|
* @csspart icon - The icon region, an image named for the tone.
|
|
42
46
|
* @csspart icon-mark - The icon for the tone, shown when no icon is slotted.
|
|
47
|
+
*
|
|
48
|
+
* @cssstate closed - While `open` is false: the callout was dismissed, and is leaving or hidden.
|
|
49
|
+
* @cssstate entering - While a callout shown again by `open` runs its entrance animations.
|
|
50
|
+
* @cssstate exiting - While a dismissed callout runs its exit animations, before it is hidden.
|
|
51
|
+
* @cssstate has-heading - Something is slotted into `heading`.
|
|
52
|
+
* @cssstate has-icon - An icon of your own is slotted into `icon`, in place of the tone's.
|
|
43
53
|
*/
|
|
44
54
|
export declare class HelixHeadlessCallout extends HelixHeadlessCallout_base {
|
|
45
55
|
/**
|
|
@@ -47,7 +57,6 @@ export declare class HelixHeadlessCallout extends HelixHeadlessCallout_base {
|
|
|
47
57
|
* `registerWords('de', { 'helix-callout': { toneLabels: { danger: 'Fehler' } } })`.
|
|
48
58
|
*/
|
|
49
59
|
static words: string;
|
|
50
|
-
protected willUpdate(changed: PropertyValues): void;
|
|
51
60
|
/**
|
|
52
61
|
* The dismiss button's name, read by screen readers, with `{label}` for
|
|
53
62
|
* what the callout is called: its heading, or the tone's name where it has
|
|
@@ -58,6 +67,23 @@ export declare class HelixHeadlessCallout extends HelixHeadlessCallout_base {
|
|
|
58
67
|
* read as written.
|
|
59
68
|
*/
|
|
60
69
|
dismissLabel: string;
|
|
70
|
+
/** Which journey is the latest, so an entrance settling late leaves an exit alone. */
|
|
71
|
+
private turn;
|
|
72
|
+
/** Hidden from a reader once hidden, for the focus hand-off. */
|
|
73
|
+
protected get present(): boolean;
|
|
74
|
+
/**
|
|
75
|
+
* Hides the callout once a dismissal stands, unless the page removed it in
|
|
76
|
+
* its listener, which is the page's answer and leaves `open` as it was.
|
|
77
|
+
*/
|
|
78
|
+
dismiss(): boolean;
|
|
79
|
+
protected willUpdate(changed: PropertyValues): void;
|
|
80
|
+
/**
|
|
81
|
+
* Whether the callout is showing. A dismissal nobody canceled turns it
|
|
82
|
+
* false and the callout hides itself, and setting it true shows the callout
|
|
83
|
+
* again. Not reflected: open is the resting state, so an `open` attribute
|
|
84
|
+
* would appear on markup a server sent without it.
|
|
85
|
+
*/
|
|
86
|
+
open: boolean;
|
|
61
87
|
/**
|
|
62
88
|
* What kind of attention the passage needs.
|
|
63
89
|
*
|
|
@@ -116,6 +142,19 @@ export declare class HelixHeadlessCallout extends HelixHeadlessCallout_base {
|
|
|
116
142
|
*/
|
|
117
143
|
private dismissName;
|
|
118
144
|
protected firstUpdated(changed: PropertyValues): void;
|
|
145
|
+
protected updated(changed: PropertyValues): void;
|
|
146
|
+
/**
|
|
147
|
+
* The way out after a dismissal, or back in after `open` is set again.
|
|
148
|
+
*
|
|
149
|
+
* **Hidden only once the exit has run**, since a hidden element draws no
|
|
150
|
+
* animation: `:state(exiting)` holds until every animation on the callout
|
|
151
|
+
* has finished, or two frames where it has none, and then `hidden` takes it
|
|
152
|
+
* out of the page and out of the accessibility tree. Shown again at once,
|
|
153
|
+
* with `:state(entering)` while it arrives.
|
|
154
|
+
*/
|
|
155
|
+
private journey;
|
|
156
|
+
/** Says which regions hold something, one call apiece so each pairs with its `@cssstate`. */
|
|
157
|
+
private readonly readRegions;
|
|
119
158
|
protected render(): TemplateResult;
|
|
120
159
|
}
|
|
121
160
|
declare global {
|
|
@@ -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, drawDismiss, drawDismissGlyph, formatWords, HelixElement, hostAria, MixinDismissible, slottedWords, TONE_MARKS, toneMark, words, wordsConverter, } from '@fusion.dev/helix-core';
|
|
7
|
+
import { animationsSettled, customElement, drawDismiss, drawDismissGlyph, formatWords, HelixElement, hostAria, MixinDismissible, slottedWords, TONE_MARKS, toneMark, words, wordsConverter, } from '@fusion.dev/helix-core';
|
|
8
8
|
import { html, nothing } from 'lit';
|
|
9
9
|
import { property } from 'lit/decorators.js';
|
|
10
10
|
import { unsafeSVG } from 'lit/directives/unsafe-svg.js';
|
|
@@ -12,6 +12,8 @@ let HelixHeadlessCallout = class HelixHeadlessCallout extends MixinDismissible(H
|
|
|
12
12
|
constructor() {
|
|
13
13
|
super(...arguments);
|
|
14
14
|
this.dismissLabel = 'Dismiss {label}';
|
|
15
|
+
this.turn = 0;
|
|
16
|
+
this.open = true;
|
|
15
17
|
this.toneLabel = '';
|
|
16
18
|
this.toneLabels = {
|
|
17
19
|
danger: 'Error',
|
|
@@ -20,14 +22,34 @@ let HelixHeadlessCallout = class HelixHeadlessCallout extends MixinDismissible(H
|
|
|
20
22
|
success: 'Success',
|
|
21
23
|
warning: 'Warning',
|
|
22
24
|
};
|
|
25
|
+
this.readRegions = () => {
|
|
26
|
+
const filled = (name) => {
|
|
27
|
+
const slot = this.renderRoot?.querySelector(`slot[name="${name}"]`);
|
|
28
|
+
return (slot?.assignedNodes({ flatten: true }) ?? []).some((node) => node.parentNode !== slot &&
|
|
29
|
+
(node.nodeType === Node.ELEMENT_NODE || node.textContent?.trim() !== ''));
|
|
30
|
+
};
|
|
31
|
+
this.toggleState('has-heading', filled('heading'));
|
|
32
|
+
this.toggleState('has-icon', filled('icon'));
|
|
33
|
+
};
|
|
23
34
|
}
|
|
24
35
|
static { this.words = 'helix-callout'; }
|
|
36
|
+
get present() {
|
|
37
|
+
return this.isConnected && !this.hasAttribute('hidden');
|
|
38
|
+
}
|
|
39
|
+
dismiss() {
|
|
40
|
+
const stands = super.dismiss();
|
|
41
|
+
if (stands && this.isConnected) {
|
|
42
|
+
this.open = false;
|
|
43
|
+
}
|
|
44
|
+
return stands;
|
|
45
|
+
}
|
|
25
46
|
willUpdate(changed) {
|
|
26
47
|
super.willUpdate(changed);
|
|
27
48
|
hostAria(this.internals, { role: 'note' });
|
|
49
|
+
this.toggleState('closed', !this.open);
|
|
28
50
|
}
|
|
29
51
|
renderIcon(icon, _label) {
|
|
30
|
-
return html `<slot name="icon" aria-hidden="true">
|
|
52
|
+
return html `<slot name="icon" aria-hidden="true" @slotchange=${this.readRegions}>
|
|
31
53
|
<svg part="icon-mark" fill="currentColor" viewBox=${icon.viewBox} aria-hidden="true">
|
|
32
54
|
${unsafeSVG(icon.content)}
|
|
33
55
|
</svg>
|
|
@@ -48,6 +70,37 @@ let HelixHeadlessCallout = class HelixHeadlessCallout extends MixinDismissible(H
|
|
|
48
70
|
super.firstUpdated(changed);
|
|
49
71
|
this.finishHydration();
|
|
50
72
|
}
|
|
73
|
+
updated(changed) {
|
|
74
|
+
super.updated(changed);
|
|
75
|
+
this.readRegions();
|
|
76
|
+
if (changed.has('open') && changed.get('open') !== undefined) {
|
|
77
|
+
this.journey();
|
|
78
|
+
}
|
|
79
|
+
else if (changed.has('open') && !this.open) {
|
|
80
|
+
this.setAttribute('hidden', '');
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
journey() {
|
|
84
|
+
this.turn += 1;
|
|
85
|
+
const turn = this.turn;
|
|
86
|
+
const open = this.open;
|
|
87
|
+
this.toggleState('entering', open);
|
|
88
|
+
this.toggleState('exiting', !open);
|
|
89
|
+
if (open) {
|
|
90
|
+
this.removeAttribute('hidden');
|
|
91
|
+
}
|
|
92
|
+
void animationsSettled(this).then(() => {
|
|
93
|
+
if (turn !== this.turn) {
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
this.toggleState('entering', false);
|
|
97
|
+
this.toggleState('exiting', false);
|
|
98
|
+
if (!open) {
|
|
99
|
+
this.setAttribute('hidden', '');
|
|
100
|
+
this.handFocusOn();
|
|
101
|
+
}
|
|
102
|
+
});
|
|
103
|
+
}
|
|
51
104
|
render() {
|
|
52
105
|
const tone = this.tone === 'neutral' ? 'neutral' : toneMark(this.tone) ? (this.tone ?? 'info') : 'info';
|
|
53
106
|
const { icon } = toneMark(tone) ?? TONE_MARKS.info;
|
|
@@ -69,6 +122,9 @@ __decorate([
|
|
|
69
122
|
words,
|
|
70
123
|
property({ attribute: 'dismiss-label' })
|
|
71
124
|
], HelixHeadlessCallout.prototype, "dismissLabel", void 0);
|
|
125
|
+
__decorate([
|
|
126
|
+
property({ type: Boolean })
|
|
127
|
+
], HelixHeadlessCallout.prototype, "open", void 0);
|
|
72
128
|
__decorate([
|
|
73
129
|
property({ reflect: true })
|
|
74
130
|
], HelixHeadlessCallout.prototype, "tone", void 0);
|
|
@@ -8,11 +8,21 @@
|
|
|
8
8
|
* depends on, out of a component library. What is left is punctuation: a header
|
|
9
9
|
* per file, a `@@` per hunk, and a first character per line.
|
|
10
10
|
*
|
|
11
|
+
* **Within a changed line it does compare.** A removed line and the added line
|
|
12
|
+
* paired with it are compared word by word, which takes two short strings and
|
|
13
|
+
* no dependency, so a reader sees which words changed without comparing two
|
|
14
|
+
* whole lines by eye.
|
|
15
|
+
*
|
|
11
16
|
* **Line numbers come from the hunk headers**, so they are the file's numbers
|
|
12
17
|
* rather than the diff's, which is the only kind a reader can look up. A line
|
|
13
18
|
* that is not in a hunk is not in the output: file headers, `index` lines and
|
|
14
19
|
* `diff --git` are metadata, and a name is taken from them and nothing else.
|
|
15
20
|
*/
|
|
21
|
+
/** A run of a changed line's text, and whether the line it is paired with lacks it. */
|
|
22
|
+
export interface DiffPiece {
|
|
23
|
+
changed: boolean;
|
|
24
|
+
text: string;
|
|
25
|
+
}
|
|
16
26
|
/** One line of a hunk, as the unified format marks it. */
|
|
17
27
|
export interface DiffLine {
|
|
18
28
|
kind: 'added' | 'context' | 'removed';
|
|
@@ -21,6 +31,11 @@ export interface DiffLine {
|
|
|
21
31
|
/** The number on the new side, absent for a line that only the old side has. */
|
|
22
32
|
new?: number;
|
|
23
33
|
text: string;
|
|
34
|
+
/**
|
|
35
|
+
* The line's text in runs, where it replaced a line or was replaced by one
|
|
36
|
+
* and the two share some words. Absent for every other line.
|
|
37
|
+
*/
|
|
38
|
+
pieces?: DiffPiece[];
|
|
24
39
|
}
|
|
25
40
|
/** A `@@` header and the lines under it. */
|
|
26
41
|
export interface DiffHunk {
|
|
@@ -51,6 +66,16 @@ export interface DiffRow {
|
|
|
51
66
|
* before it rather than a line of its own, so it is dropped.
|
|
52
67
|
*/
|
|
53
68
|
export declare const parsePatch: (patch: string) => DiffFile[];
|
|
69
|
+
/**
|
|
70
|
+
* The runs of two paired lines that the other line does not have, by the
|
|
71
|
+
* longest run of words they share in order.
|
|
72
|
+
*
|
|
73
|
+
* **Space between two changes is part of the change**, so "a b" replaced by
|
|
74
|
+
* "c d" is one run, with no gap for the eye to bridge. Two lines
|
|
75
|
+
* that share no word share nothing worth marking: the whole line is the
|
|
76
|
+
* change, which its own wash already says, and `undefined` draws it whole.
|
|
77
|
+
*/
|
|
78
|
+
export declare const changedWords: (before: string, after: string) => [DiffPiece[], DiffPiece[]] | undefined;
|
|
54
79
|
/**
|
|
55
80
|
* A hunk's lines paired for a side-by-side view.
|
|
56
81
|
*
|
|
@@ -60,3 +85,20 @@ export declare const parsePatch: (patch: string) => DiffFile[];
|
|
|
60
85
|
* across rather than hunting for it further down.
|
|
61
86
|
*/
|
|
62
87
|
export declare const pairLines: (lines: DiffLine[]) => DiffRow[];
|
|
88
|
+
/** A stretch of a hunk's lines: drawn, or unchanged lines folded behind a control. */
|
|
89
|
+
export interface DiffRun {
|
|
90
|
+
/** Whether the lines are unchanged ones folded away. */
|
|
91
|
+
folded: boolean;
|
|
92
|
+
lines: DiffLine[];
|
|
93
|
+
/** Where the run starts among the hunk's lines, which names a fold. */
|
|
94
|
+
start: number;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* A hunk's lines with each long run of unchanged lines folded, keeping `keep`
|
|
98
|
+
* lines on the side of each change, as a code host draws a diff with more
|
|
99
|
+
* context than a reader needs.
|
|
100
|
+
*
|
|
101
|
+
* A fold hides two lines or more: a control in place of one line is no
|
|
102
|
+
* shorter than the line.
|
|
103
|
+
*/
|
|
104
|
+
export declare const foldContext: (lines: DiffLine[], keep: number) => DiffRun[];
|
package/dist/code-block/patch.js
CHANGED
|
@@ -63,8 +63,79 @@ export const parsePatch = (patch) => {
|
|
|
63
63
|
}
|
|
64
64
|
}
|
|
65
65
|
}
|
|
66
|
+
for (const one of files) {
|
|
67
|
+
for (const { lines } of one.hunks) {
|
|
68
|
+
markWords(lines);
|
|
69
|
+
}
|
|
70
|
+
}
|
|
66
71
|
return files;
|
|
67
72
|
};
|
|
73
|
+
const TOKEN = /[\p{L}\p{N}_]+|\s+|[^\p{L}\p{N}_\s]/gu;
|
|
74
|
+
const MOST_TOKENS = 200;
|
|
75
|
+
const joined = (pieces) => {
|
|
76
|
+
const out = [];
|
|
77
|
+
for (const piece of pieces) {
|
|
78
|
+
const last = out.at(-1);
|
|
79
|
+
if (last && last.changed === piece.changed) {
|
|
80
|
+
last.text += piece.text;
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
out.push({ ...piece });
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
return out;
|
|
87
|
+
};
|
|
88
|
+
export const changedWords = (before, after) => {
|
|
89
|
+
const a = before.match(TOKEN) ?? [];
|
|
90
|
+
const b = after.match(TOKEN) ?? [];
|
|
91
|
+
if (a.length === 0 || b.length === 0 || a.length > MOST_TOKENS || b.length > MOST_TOKENS) {
|
|
92
|
+
return undefined;
|
|
93
|
+
}
|
|
94
|
+
const width = b.length + 1;
|
|
95
|
+
const table = new Uint16Array((a.length + 1) * width);
|
|
96
|
+
for (let i = a.length - 1; i >= 0; i -= 1) {
|
|
97
|
+
for (let j = b.length - 1; j >= 0; j -= 1) {
|
|
98
|
+
table[i * width + j] =
|
|
99
|
+
a[i] === b[j]
|
|
100
|
+
? table[(i + 1) * width + j + 1] + 1
|
|
101
|
+
: Math.max(table[(i + 1) * width + j], table[i * width + j + 1]);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
const left = [];
|
|
105
|
+
const right = [];
|
|
106
|
+
let shared = false;
|
|
107
|
+
let i = 0;
|
|
108
|
+
let j = 0;
|
|
109
|
+
while (i < a.length || j < b.length) {
|
|
110
|
+
if (i < a.length && j < b.length && a[i] === b[j]) {
|
|
111
|
+
shared ||= a[i].trim() !== '';
|
|
112
|
+
left.push({ changed: false, text: a[i] });
|
|
113
|
+
right.push({ changed: false, text: b[j] });
|
|
114
|
+
i += 1;
|
|
115
|
+
j += 1;
|
|
116
|
+
}
|
|
117
|
+
else if (j >= b.length ||
|
|
118
|
+
(i < a.length &&
|
|
119
|
+
table[(i + 1) * width + j] >= table[i * width + j + 1])) {
|
|
120
|
+
left.push({ changed: true, text: a[i] });
|
|
121
|
+
i += 1;
|
|
122
|
+
}
|
|
123
|
+
else {
|
|
124
|
+
right.push({ changed: true, text: b[j] });
|
|
125
|
+
j += 1;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (!shared) {
|
|
129
|
+
return undefined;
|
|
130
|
+
}
|
|
131
|
+
const bridged = (pieces) => joined(pieces.map((piece, at) => !piece.changed &&
|
|
132
|
+
piece.text.trim() === '' &&
|
|
133
|
+
pieces[at - 1]?.changed &&
|
|
134
|
+
pieces[at + 1]?.changed
|
|
135
|
+
? { ...piece, changed: true }
|
|
136
|
+
: piece));
|
|
137
|
+
return [bridged(left), bridged(right)];
|
|
138
|
+
};
|
|
68
139
|
export const pairLines = (lines) => {
|
|
69
140
|
const rows = [];
|
|
70
141
|
let at = 0;
|
|
@@ -91,3 +162,50 @@ export const pairLines = (lines) => {
|
|
|
91
162
|
}
|
|
92
163
|
return rows;
|
|
93
164
|
};
|
|
165
|
+
const markWords = (lines) => {
|
|
166
|
+
for (const { left, right } of pairLines(lines)) {
|
|
167
|
+
if (left?.kind !== 'removed' || right?.kind !== 'added') {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
const pieces = changedWords(left.text, right.text);
|
|
171
|
+
if (pieces) {
|
|
172
|
+
;
|
|
173
|
+
[left.pieces, right.pieces] = pieces;
|
|
174
|
+
}
|
|
175
|
+
}
|
|
176
|
+
};
|
|
177
|
+
export const foldContext = (lines, keep) => {
|
|
178
|
+
const runs = [];
|
|
179
|
+
const add = (start, end, folded) => {
|
|
180
|
+
if (end <= start) {
|
|
181
|
+
return;
|
|
182
|
+
}
|
|
183
|
+
const last = runs.at(-1);
|
|
184
|
+
if (last && !last.folded && !folded) {
|
|
185
|
+
last.lines.push(...lines.slice(start, end));
|
|
186
|
+
}
|
|
187
|
+
else {
|
|
188
|
+
runs.push({ folded, lines: lines.slice(start, end), start });
|
|
189
|
+
}
|
|
190
|
+
};
|
|
191
|
+
let at = 0;
|
|
192
|
+
while (at < lines.length) {
|
|
193
|
+
const context = lines[at]?.kind === 'context';
|
|
194
|
+
let end = at;
|
|
195
|
+
while (end < lines.length && (lines[end]?.kind === 'context') === context) {
|
|
196
|
+
end += 1;
|
|
197
|
+
}
|
|
198
|
+
const head = context && at > 0 ? keep : 0;
|
|
199
|
+
const tail = context && end < lines.length ? keep : 0;
|
|
200
|
+
if (context && end - at - head - tail >= 2) {
|
|
201
|
+
add(at, at + head, false);
|
|
202
|
+
add(at + head, end - tail, true);
|
|
203
|
+
add(end - tail, end, false);
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
add(at, end, false);
|
|
207
|
+
}
|
|
208
|
+
at = end;
|
|
209
|
+
}
|
|
210
|
+
return runs;
|
|
211
|
+
};
|
|
@@ -45,9 +45,14 @@ import '../../copy-button/index.js';
|
|
|
45
45
|
* removed. The block never computes a diff itself. Line numbers come from the
|
|
46
46
|
* hunk headers and are hidden from the accessibility tree. Changed lines are
|
|
47
47
|
* `<ins>` and `<del>` with the `+` and `-` kept as text, so the change does not
|
|
48
|
-
* depend on color.
|
|
48
|
+
* depend on color. Where a removed line is paired with the added line that
|
|
49
|
+
* replaced it, the words each does not share with the other are the `changed`
|
|
50
|
+
* part, so the eye goes to what changed within the line. Each file in the patch is its own block under its own name,
|
|
49
51
|
* and its copy button copies the new version of the file. `variant="split"`
|
|
50
52
|
* shows the old version beside the new, each removal opposite its replacement.
|
|
53
|
+
* `context-lines` folds each long run of unchanged lines, keeping that many
|
|
54
|
+
* beside each change, behind a button that shows them and hides them again,
|
|
55
|
+
* as a code host draws a diff written with more context than a reader needs.
|
|
51
56
|
*
|
|
52
57
|
* **A block that scrolls is a tab stop.** While its lines run past its width or
|
|
53
58
|
* its cap, the scroller is a region named for the block, such as "Code,
|
|
@@ -83,6 +88,7 @@ import '../../copy-button/index.js';
|
|
|
83
88
|
* @csspart actions - The box in the header that holds the `actions` slot, drawn while something is slotted there.
|
|
84
89
|
* @csspart added - In a diff, a line only the new version has, an `<ins>`.
|
|
85
90
|
* @csspart block - The scrolling `<pre>`.
|
|
91
|
+
* @csspart changed - In a diff, the words of a changed line that the line opposite it does not have, inside its `text`.
|
|
86
92
|
* @csspart code - The `<code>` inside it.
|
|
87
93
|
* @csspart context - In a diff, a line both versions share.
|
|
88
94
|
* @csspart copy - The copy button. It sits in the header when there is one, and beside the code otherwise.
|
|
@@ -104,6 +110,7 @@ import '../../copy-button/index.js';
|
|
|
104
110
|
* @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.
|
|
105
111
|
* @csspart file - In a diff, one file's header and block.
|
|
106
112
|
* @csspart filler - In a split diff, the empty row opposite a line the other side does not have.
|
|
113
|
+
* @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`.
|
|
107
114
|
* @csspart frame - The code, and the copy button beside it where there is no header.
|
|
108
115
|
* @csspart gutter - The line numbers beside the code, shown when `line-numbers` is set.
|
|
109
116
|
* @csspart header - The bar that holds the name and the copy button, shown when there is a name.
|
|
@@ -148,6 +155,12 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
148
155
|
* The expand button's words while the block shows every line.
|
|
149
156
|
*/
|
|
150
157
|
collapseLabel: string;
|
|
158
|
+
/**
|
|
159
|
+
* In a diff, how many unchanged lines to keep beside each change. Each
|
|
160
|
+
* longer run of unchanged lines is folded behind a button that shows it.
|
|
161
|
+
* Unset shows every line the diff holds.
|
|
162
|
+
*/
|
|
163
|
+
contextLines?: number;
|
|
151
164
|
/**
|
|
152
165
|
* The copy button's name and tooltip text after a copy.
|
|
153
166
|
*/
|
|
@@ -185,6 +198,12 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
185
198
|
* The expand button's words while the cap hides lines.
|
|
186
199
|
*/
|
|
187
200
|
expandLabel: string;
|
|
201
|
+
/**
|
|
202
|
+
* The words of a button that hides a run of unchanged lines it showed, in a
|
|
203
|
+
* diff with `context-lines`. `{count}` is how many, and each language
|
|
204
|
+
* writes the forms it has.
|
|
205
|
+
*/
|
|
206
|
+
hideUnchangedLabel: string;
|
|
188
207
|
/**
|
|
189
208
|
* Turns the code into highlighted markup, which the block draws in place of
|
|
190
209
|
* the plain text.
|
|
@@ -263,6 +282,11 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
263
282
|
* `false`.
|
|
264
283
|
*/
|
|
265
284
|
noCopy: boolean;
|
|
285
|
+
/**
|
|
286
|
+
* The words of a button that shows a folded run of unchanged lines, in a
|
|
287
|
+
* diff with `context-lines`. `{count}` is how many.
|
|
288
|
+
*/
|
|
289
|
+
showUnchangedLabel: string;
|
|
266
290
|
/**
|
|
267
291
|
* How a diff's two versions are arranged. It has no effect without `diff`.
|
|
268
292
|
*
|
|
@@ -295,6 +319,18 @@ export declare class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
295
319
|
private parsed;
|
|
296
320
|
/** The files in the content, when it is a diff and it can be read yet. */
|
|
297
321
|
private get files();
|
|
322
|
+
/** The folds a reader opened, by file, hunk and the line each starts at. */
|
|
323
|
+
private readonly unfolded;
|
|
324
|
+
/** A hunk's lines in runs, folded where `context-lines` asks. */
|
|
325
|
+
private runsOf;
|
|
326
|
+
/**
|
|
327
|
+
* The button over a folded run, and the run once it is open.
|
|
328
|
+
*
|
|
329
|
+
* A disclosure, so focus stays on the button as the lines appear under it.
|
|
330
|
+
* The new side of a split diff draws the same words hidden from the
|
|
331
|
+
* accessibility tree, there to keep the rows level and to answer a press.
|
|
332
|
+
*/
|
|
333
|
+
private renderFold;
|
|
298
334
|
/**
|
|
299
335
|
* The code as text, read from what the default slot is given rather than
|
|
300
336
|
* stored.
|
|
@@ -12,7 +12,7 @@ import { html } from 'lit/static-html.js';
|
|
|
12
12
|
import '../../copy-button/index.js';
|
|
13
13
|
import { highlightedLines } from '../highlight.js';
|
|
14
14
|
import { gutterNumbers, lineCount, lineRuns, lineSpans } from '../lines.js';
|
|
15
|
-
import { pairLines, parsePatch } from '../patch.js';
|
|
15
|
+
import { foldContext, pairLines, parsePatch, } from '../patch.js';
|
|
16
16
|
let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
17
17
|
constructor() {
|
|
18
18
|
super(...arguments);
|
|
@@ -25,15 +25,18 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
25
25
|
this.diff = false;
|
|
26
26
|
this.expandable = false;
|
|
27
27
|
this.expandLabel = 'Show all lines';
|
|
28
|
+
this.hideUnchangedLabel = '{count, plural, one {Hide # unchanged line} other {Hide # unchanged lines}}';
|
|
28
29
|
this.label = '';
|
|
29
30
|
this.language = '';
|
|
30
31
|
this.lineNumbers = false;
|
|
31
32
|
this.markLines = '';
|
|
32
33
|
this.noCopy = false;
|
|
34
|
+
this.showUnchangedLabel = '{count, plural, one {Show # unchanged line} other {Show # unchanged lines}}';
|
|
33
35
|
this.wrap = false;
|
|
34
36
|
this.wrapLabel = 'Wrap lines';
|
|
35
37
|
this.wrappable = false;
|
|
36
38
|
this.parsed = { files: [], text: '' };
|
|
39
|
+
this.unfolded = new Set();
|
|
37
40
|
this.capped = false;
|
|
38
41
|
this.expanded = false;
|
|
39
42
|
this.measure = () => {
|
|
@@ -69,9 +72,32 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
69
72
|
const text = this.text;
|
|
70
73
|
if (text !== this.parsed.text) {
|
|
71
74
|
this.parsed = { files: parsePatch(text), text };
|
|
75
|
+
this.unfolded.clear();
|
|
72
76
|
}
|
|
73
77
|
return this.parsed.files;
|
|
74
78
|
}
|
|
79
|
+
runsOf(hunk) {
|
|
80
|
+
const keep = this.contextLines;
|
|
81
|
+
return keep === undefined || keep < 0
|
|
82
|
+
? [{ folded: false, lines: hunk.lines, start: 0 }]
|
|
83
|
+
: foldContext(hunk.lines, Math.floor(keep));
|
|
84
|
+
}
|
|
85
|
+
renderFold(run, key, line, hidden = false) {
|
|
86
|
+
const open = this.unfolded.has(key);
|
|
87
|
+
const words = formatWords(this, open ? 'hideUnchangedLabel' : 'showUnchangedLabel', {
|
|
88
|
+
count: run.lines.length,
|
|
89
|
+
});
|
|
90
|
+
const toggle = () => {
|
|
91
|
+
if (!this.unfolded.delete(key)) {
|
|
92
|
+
this.unfolded.add(key);
|
|
93
|
+
}
|
|
94
|
+
this.requestUpdate();
|
|
95
|
+
};
|
|
96
|
+
const control = hidden
|
|
97
|
+
? html `<span part="fold" aria-hidden="true" @click=${toggle}>${words}</span>`
|
|
98
|
+
: html `<button part="fold" type="button" aria-expanded=${open ? 'true' : 'false'} @click=${toggle}>${words}</button>`;
|
|
99
|
+
return html `${control}\n${open ? run.lines.map(line) : nothing}`;
|
|
100
|
+
}
|
|
75
101
|
get text() {
|
|
76
102
|
if (this.hydrating) {
|
|
77
103
|
return '';
|
|
@@ -319,7 +345,10 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
319
345
|
const numbers = side
|
|
320
346
|
? this.renderNumber(side === 'old' ? line.old : line.new, width)
|
|
321
347
|
: html `${this.renderNumber(line.old, width)}${this.renderNumber(line.new, width)}`;
|
|
322
|
-
const
|
|
348
|
+
const words = line.pieces
|
|
349
|
+
? line.pieces.map((piece) => piece.changed ? html `<span part="changed">${piece.text}</span>` : piece.text)
|
|
350
|
+
: line.text;
|
|
351
|
+
const text = html `<span part="text">${words}\n</span>`;
|
|
323
352
|
if (line.kind === 'added') {
|
|
324
353
|
return html `<ins part="added">${numbers}<span part="marker">+</span>${text}</ins>`;
|
|
325
354
|
}
|
|
@@ -331,16 +360,22 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
331
360
|
renderHunk(hunk, hidden = false) {
|
|
332
361
|
return html `<span part="hunk" aria-hidden=${hidden ? 'true' : nothing}>${hunk.header}\n</span>`;
|
|
333
362
|
}
|
|
334
|
-
renderSplit(file, width) {
|
|
335
|
-
const hunks = file.hunks.map((hunk) => ({ hunk,
|
|
363
|
+
renderSplit(file, width, at) {
|
|
364
|
+
const hunks = file.hunks.map((hunk) => ({ hunk, runs: this.runsOf(hunk) }));
|
|
336
365
|
const filler = html `<span part="filler" aria-hidden="true">\n</span>`;
|
|
366
|
+
const side = (old) => hunks.map(({ hunk, runs }, h) => html `${this.renderHunk(hunk, !old)}${runs.map((run) => run.folded
|
|
367
|
+
? this.renderFold(run, `${at}:${h}:${run.start}`, (line) => this.renderLine(line, width, old ? 'old' : 'new'), !old)
|
|
368
|
+
: pairLines(run.lines).map(({ left, right }) => {
|
|
369
|
+
const line = old ? left : right;
|
|
370
|
+
return line ? this.renderLine(line, width, old ? 'old' : 'new') : filler;
|
|
371
|
+
}))}`);
|
|
337
372
|
return html `<div part="split">
|
|
338
|
-
<pre part="old" translate="no"><code part="code">${
|
|
339
|
-
<pre part="new" translate="no"><code part="code">${
|
|
373
|
+
<pre part="old" translate="no"><code part="code">${side(true)}</code></pre>
|
|
374
|
+
<pre part="new" translate="no"><code part="code">${side(false)}</code></pre>
|
|
340
375
|
</div>`;
|
|
341
376
|
}
|
|
342
377
|
renderDiff(files) {
|
|
343
|
-
return html `${files.map((file) => {
|
|
378
|
+
return html `${files.map((file, at) => {
|
|
344
379
|
const name = file.name || this.label;
|
|
345
380
|
const widest = Math.max(0, ...file.hunks.flatMap((hunk) => hunk.lines.map((line) => Math.max(line.old ?? 0, line.new ?? 0))));
|
|
346
381
|
const width = String(widest).length;
|
|
@@ -352,8 +387,10 @@ let HelixHeadlessCodeBlock = class HelixHeadlessCodeBlock extends HelixElement {
|
|
|
352
387
|
${this.renderHeader(name, now)}
|
|
353
388
|
<div part="frame">
|
|
354
389
|
${this.variant === 'split'
|
|
355
|
-
? this.renderSplit(file, width)
|
|
356
|
-
: html `<pre part="block" translate="no"><code part="code">${file.hunks.map((hunk) => html `${this.renderHunk(hunk)}${hunk.
|
|
390
|
+
? this.renderSplit(file, width, at)
|
|
391
|
+
: html `<pre part="block" translate="no"><code part="code">${file.hunks.map((hunk, h) => html `${this.renderHunk(hunk)}${this.runsOf(hunk).map((run) => run.folded
|
|
392
|
+
? this.renderFold(run, `${at}:${h}:${run.start}`, (line) => this.renderLine(line, width))
|
|
393
|
+
: run.lines.map((line) => this.renderLine(line, width)))}`)}</code></pre>`}
|
|
357
394
|
${this.headed(name) ? nothing : this.renderCopy(now)}
|
|
358
395
|
</div>
|
|
359
396
|
</div>`;
|
|
@@ -372,6 +409,9 @@ __decorate([
|
|
|
372
409
|
words,
|
|
373
410
|
property({ attribute: 'collapse-label' })
|
|
374
411
|
], HelixHeadlessCodeBlock.prototype, "collapseLabel", void 0);
|
|
412
|
+
__decorate([
|
|
413
|
+
property({ attribute: 'context-lines', type: Number })
|
|
414
|
+
], HelixHeadlessCodeBlock.prototype, "contextLines", void 0);
|
|
375
415
|
__decorate([
|
|
376
416
|
words,
|
|
377
417
|
property({ attribute: 'copied-label' })
|
|
@@ -394,6 +434,10 @@ __decorate([
|
|
|
394
434
|
words,
|
|
395
435
|
property({ attribute: 'expand-label' })
|
|
396
436
|
], HelixHeadlessCodeBlock.prototype, "expandLabel", void 0);
|
|
437
|
+
__decorate([
|
|
438
|
+
words,
|
|
439
|
+
property({ attribute: 'hide-unchanged-label' })
|
|
440
|
+
], HelixHeadlessCodeBlock.prototype, "hideUnchangedLabel", void 0);
|
|
397
441
|
__decorate([
|
|
398
442
|
property({ attribute: false })
|
|
399
443
|
], HelixHeadlessCodeBlock.prototype, "highlight", void 0);
|
|
@@ -412,6 +456,10 @@ __decorate([
|
|
|
412
456
|
__decorate([
|
|
413
457
|
property({ attribute: 'no-copy', reflect: true, type: Boolean })
|
|
414
458
|
], HelixHeadlessCodeBlock.prototype, "noCopy", void 0);
|
|
459
|
+
__decorate([
|
|
460
|
+
words,
|
|
461
|
+
property({ attribute: 'show-unchanged-label' })
|
|
462
|
+
], HelixHeadlessCodeBlock.prototype, "showUnchangedLabel", void 0);
|
|
415
463
|
__decorate([
|
|
416
464
|
property({ reflect: true })
|
|
417
465
|
], HelixHeadlessCodeBlock.prototype, "variant", void 0);
|
|
@@ -22,8 +22,10 @@ declare const HelixHeadlessColorPicker_base: (new (...args: any[]) => import("@f
|
|
|
22
22
|
* gray after you drag to the bottom of the square and back.
|
|
23
23
|
*
|
|
24
24
|
* The square is one tab stop. In it, the up and down arrows move brightness
|
|
25
|
-
* and the left and right arrows move saturation,
|
|
26
|
-
* brightness by ten
|
|
25
|
+
* and the left and right arrows move saturation, by ten with Shift. PageUp and
|
|
26
|
+
* PageDown move brightness by ten, and Home and End move saturation by ten,
|
|
27
|
+
* turned on a right-to-left page as the arrows are, as React Aria's ColorArea
|
|
28
|
+
* does. The picker sends `input` as the color moves and `change`
|
|
27
29
|
* when a drag, a key, a typed value or a preset settles it, as
|
|
28
30
|
* `<input type="color">` does.
|
|
29
31
|
*
|
|
@@ -546,7 +548,9 @@ export declare class HelixHeadlessColorPicker extends HelixHeadlessColorPicker_b
|
|
|
546
548
|
* its own one axis: on the saturation input, ArrowUp moved the handle
|
|
547
549
|
* sideways. Here the vertical keys move brightness and the horizontal keys
|
|
548
550
|
* move saturation, turned on a right-to-left page as the pointer is, which is
|
|
549
|
-
* what React Aria's ColorArea does.
|
|
551
|
+
* what React Aria's ColorArea does. Shift makes an arrow a step of ten, and
|
|
552
|
+
* Home and End are saturation's steps of ten, as PageUp and PageDown are
|
|
553
|
+
* brightness's.
|
|
550
554
|
*/
|
|
551
555
|
private readonly handleAreaKeydown;
|
|
552
556
|
/**
|
|
@@ -211,11 +211,14 @@ let HelixHeadlessColorPicker = class HelixHeadlessColorPicker extends MixinPicke
|
|
|
211
211
|
const color = this.color;
|
|
212
212
|
const along = isRtl(this) ? -1 : 1;
|
|
213
213
|
const held = (at) => clamp(at, 0, 100);
|
|
214
|
+
const step = event.shiftKey ? 10 : 1;
|
|
214
215
|
const moves = {
|
|
215
|
-
ArrowDown: { brightness: held(color.brightness -
|
|
216
|
-
ArrowLeft: { saturation: held(color.saturation - along) },
|
|
217
|
-
ArrowRight: { saturation: held(color.saturation + along) },
|
|
218
|
-
ArrowUp: { brightness: held(color.brightness +
|
|
216
|
+
ArrowDown: { brightness: held(color.brightness - step) },
|
|
217
|
+
ArrowLeft: { saturation: held(color.saturation - along * step) },
|
|
218
|
+
ArrowRight: { saturation: held(color.saturation + along * step) },
|
|
219
|
+
ArrowUp: { brightness: held(color.brightness + step) },
|
|
220
|
+
End: { saturation: held(color.saturation + along * 10) },
|
|
221
|
+
Home: { saturation: held(color.saturation - along * 10) },
|
|
219
222
|
PageDown: { brightness: held(color.brightness - 10) },
|
|
220
223
|
PageUp: { brightness: held(color.brightness + 10) },
|
|
221
224
|
};
|