@mdbase-dev/ui 0.1.0-beta.125 → 0.1.0-beta.147

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 (77) hide show
  1. package/README.md +159 -0
  2. package/css/brand.css +10 -10
  3. package/css/collections.css +978 -0
  4. package/css/controls.css +220 -43
  5. package/css/feedback.css +20 -20
  6. package/css/fonts.css +2 -3
  7. package/css/palette.css +10 -10
  8. package/css/screens.css +58 -16
  9. package/css/tokens.css +43 -3
  10. package/dist/AppSwitcher.js +187 -33
  11. package/dist/Checkbox.d.ts +14 -0
  12. package/dist/Checkbox.js +32 -0
  13. package/dist/CommandPalette.d.ts +4 -2
  14. package/dist/CommandPalette.js +275 -103
  15. package/dist/Dialog.js +72 -25
  16. package/dist/Feedback.js +763 -229
  17. package/dist/FeedbackMarkup.d.ts +1 -1
  18. package/dist/FeedbackMarkup.js +326 -125
  19. package/dist/FeedbackVerification.js +115 -68
  20. package/dist/SaveNotice.js +71 -28
  21. package/dist/Select.js +183 -135
  22. package/dist/SelectList.js +59 -14
  23. package/dist/ThemeSelect.js +55 -4
  24. package/dist/apps.d.ts +1 -0
  25. package/dist/apps.js +50 -29
  26. package/dist/brand.js +251 -68
  27. package/dist/codemirror.js +116 -87
  28. package/dist/collections/CollectionGate.d.ts +52 -0
  29. package/dist/collections/CollectionGate.js +456 -0
  30. package/dist/collections/CollectionSaveWarning.d.ts +10 -0
  31. package/dist/collections/CollectionSaveWarning.js +28 -0
  32. package/dist/collections/CollectionSwitcher.d.ts +26 -0
  33. package/dist/collections/CollectionSwitcher.js +579 -0
  34. package/dist/collections/SetupReview.d.ts +9 -0
  35. package/dist/collections/SetupReview.js +175 -0
  36. package/dist/collections/copy.d.ts +100 -0
  37. package/dist/collections/copy.js +937 -0
  38. package/dist/collections/dialogs.d.ts +27 -0
  39. package/dist/collections/dialogs.js +169 -0
  40. package/dist/collections/model.d.ts +146 -0
  41. package/dist/collections/model.js +337 -0
  42. package/dist/collections/mount.d.ts +18 -0
  43. package/dist/collections/mount.js +56 -0
  44. package/dist/collections/parts.d.ts +86 -0
  45. package/dist/collections/parts.js +439 -0
  46. package/dist/collections/testing.d.ts +19 -0
  47. package/dist/collections/testing.js +39 -0
  48. package/dist/collections/use-collection-ui.d.ts +140 -0
  49. package/dist/collections/use-collection-ui.js +236 -0
  50. package/dist/collections.d.ts +19 -0
  51. package/dist/collections.js +13 -0
  52. package/dist/feedback-data.d.ts +13 -12
  53. package/dist/feedback-data.js +365 -168
  54. package/dist/flock.d.ts +63 -0
  55. package/dist/flock.js +140 -0
  56. package/dist/i18n.d.ts +6 -0
  57. package/dist/i18n.js +71 -0
  58. package/dist/locales/de/messages.json +1 -0
  59. package/dist/locales/en/messages.json +1 -0
  60. package/dist/locales/es/messages.json +1 -0
  61. package/dist/locales/fr/messages.json +1 -0
  62. package/dist/locales/it/messages.json +1 -0
  63. package/dist/locales/ja/messages.json +1 -0
  64. package/dist/locales/ko/messages.json +1 -0
  65. package/dist/locales/pt/messages.json +1 -0
  66. package/dist/locales/ru/messages.json +1 -0
  67. package/dist/locales/zh/messages.json +1 -0
  68. package/dist/mark-activity.js +82 -75
  69. package/dist/permission-selection.js +27 -17
  70. package/dist/popover.js +138 -130
  71. package/dist/screens.d.ts +20 -3
  72. package/dist/screens.js +275 -6
  73. package/dist/select-model.js +34 -28
  74. package/dist/text-diff.d.ts +1 -0
  75. package/dist/text-diff.js +106 -74
  76. package/dist/theme.js +34 -40
  77. package/package.json +53 -8
package/dist/screens.js CHANGED
@@ -1,20 +1,289 @@
1
1
  import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
2
- import { Wordmark } from "./brand.js";
2
+ import { i18n as _i18n } from "@lingui/core";
3
+ import { useLocaleRevision } from "./i18n.js";
4
+ import { useEffect, useRef, useState, useSyncExternalStore } from "react";
5
+ import { MdbaseMark, Wordmark } from "./brand.js";
6
+ import { FLOCK_TIMING, flockFrame } from "./flock.js";
3
7
  /**
4
8
  * What an app shows while it opens a collection: its wordmark, scanning, and a quiet line
5
9
  * saying what it is waiting for. A failure keeps the same place, shakes the mark once and
6
10
  * offers a retry.
11
+ *
12
+ * With `entrance="flock"` it is the launch screen: a 42px mark that the flock flies onto
13
+ * from the edges of the screen, once per page, before it starts scanning. Pair it with
14
+ * `useLaunchEntrancePlaying()` so a fast open does not cut the entrance off.
7
15
  */
8
- export function OpeningScreen({ app, title, detail, error, onRetry }) {
9
- return _jsx("main", { className: "mdbase-opening", "data-loading-state": error ? "failed" : "opening", "aria-label": title, "aria-busy": !error, children: _jsxs("div", { className: "mdbase-opening-message", role: error ? "alert" : "status", children: [_jsx(Wordmark, { app: app, motion: "scan", signal: error ? openingFailed : null }), _jsxs("div", { children: [_jsx("p", { children: error ?? title }), !error && detail && _jsx("small", { children: detail })] }), error && onRetry && _jsx("button", { type: "button", className: "mdbase-button", onClick: onRetry, children: "Try again" })] }) });
16
+ export function OpeningScreen(props) {
17
+ useLocaleRevision();
18
+ if (props.entrance === "flock" && typeof props.app === "string") return _jsx(LaunchScreen, {
19
+ ...props,
20
+ app: props.app
21
+ });
22
+ const {
23
+ app,
24
+ title,
25
+ detail,
26
+ error,
27
+ onRetry
28
+ } = props;
29
+ const Container = props.layout === "panel" ? "div" : "main";
30
+ return _jsx(Container, {
31
+ className: "mdbase-opening",
32
+ "data-loading-state": error ? "failed" : "opening",
33
+ "aria-label": title,
34
+ "aria-busy": !error,
35
+ children: _jsxs("div", {
36
+ className: "mdbase-opening-message",
37
+ role: error ? "alert" : "status",
38
+ children: [typeof app === "string" ? _jsx(Wordmark, {
39
+ app: app,
40
+ motion: "scan",
41
+ signal: error ? openingFailed : null
42
+ }) : _jsxs("span", {
43
+ className: "wordmark",
44
+ children: [_jsx("span", {
45
+ className: "wordmark-mark mdbase-motion-scan",
46
+ "aria-hidden": "true",
47
+ children: app.mark ?? _jsx(MdbaseMark, {
48
+ signal: error ? openingFailed : null
49
+ })
50
+ }), _jsx("span", {
51
+ className: "wordmark-label",
52
+ children: app.name
53
+ })]
54
+ }), _jsxs("div", {
55
+ children: [_jsx("p", {
56
+ children: error ?? title
57
+ }), !error && detail && _jsx("small", {
58
+ children: detail
59
+ })]
60
+ }), error && onRetry && _jsx("button", {
61
+ type: "button",
62
+ className: "mdbase-button",
63
+ onClick: onRetry,
64
+ children: _i18n._(
65
+ /** i18n */
66
+ {
67
+ id: "KDw4GX",
68
+ message: "Try again"
69
+ })
70
+ })]
71
+ })
72
+ });
73
+ }
74
+ const openingFailed = {
75
+ kind: "error",
76
+ id: 1
77
+ };
78
+ function LaunchScreen({
79
+ app,
80
+ title,
81
+ detail,
82
+ error,
83
+ onRetry
84
+ }) {
85
+ useLocaleRevision();
86
+ const markHost = useRef(null);
87
+ const {
88
+ flying,
89
+ canvas
90
+ } = useFlockEntrance(markHost);
91
+ return _jsxs("main", {
92
+ className: "mdbase-opening mdbase-opening-launch",
93
+ "data-loading-state": error ? "failed" : "opening",
94
+ "data-entrance": flying ? "flying" : "landed",
95
+ "aria-label": title,
96
+ "aria-busy": !error,
97
+ children: [_jsxs("div", {
98
+ className: "mdbase-opening-message",
99
+ role: error ? "alert" : "status",
100
+ children: [_jsx("span", {
101
+ ref: markHost,
102
+ className: "mdbase-opening-launch-mark",
103
+ children: _jsx(MdbaseMark, {
104
+ app: app === "editor" ? undefined : app,
105
+ motion: flying || error ? undefined : "scan",
106
+ signal: error ? openingFailed : null
107
+ })
108
+ }), _jsxs("div", {
109
+ className: "mdbase-opening-launch-text",
110
+ children: [_jsx("p", {
111
+ children: error ?? title
112
+ }), !error && detail && _jsx("small", {
113
+ children: detail
114
+ })]
115
+ }), error && onRetry && _jsx("button", {
116
+ type: "button",
117
+ className: "mdbase-button",
118
+ onClick: onRetry,
119
+ children: _i18n._(
120
+ /** i18n */
121
+ {
122
+ id: "KDw4GX",
123
+ message: "Try again"
124
+ })
125
+ })]
126
+ }), canvas]
127
+ });
10
128
  }
11
- const openingFailed = { kind: "error", id: 1 };
12
129
  /**
13
130
  * The screen before a collection is open: the app's wordmark, a headline, what the app will
14
131
  * do, what went wrong if anything, and then the actions (children) for connecting. A status
15
132
  * and an error that say the same thing are said once; technical detail waits behind a
16
133
  * disclosure.
17
134
  */
18
- export function ConnectLayout({ app, title, lead, status, error, detail, footnote, children }) {
19
- return _jsx("main", { className: "mdbase-connect-screen", children: _jsxs("section", { children: [_jsx(Wordmark, { app: app, motion: "assemble" }), _jsx("h1", { children: title }), lead && _jsx("p", { className: "mdbase-connect-lead", children: lead }), status && status !== error && _jsx("p", { className: "mdbase-connect-status", role: "status", children: status }), error && _jsx("p", { className: "mdbase-connect-error", role: "alert", children: error }), detail && _jsxs("details", { className: "mdbase-connect-detail", children: [_jsx("summary", { children: "Details" }), _jsx("code", { children: detail })] }), children, footnote && _jsx("p", { className: "mdbase-connect-footnote", children: footnote })] }) });
135
+ export function ConnectLayout({
136
+ app,
137
+ title,
138
+ lead,
139
+ status,
140
+ error,
141
+ detail,
142
+ footnote,
143
+ children
144
+ }) {
145
+ useLocaleRevision();
146
+ return _jsx("main", {
147
+ className: "mdbase-connect-screen",
148
+ children: _jsxs("section", {
149
+ children: [_jsx(Wordmark, {
150
+ app: app,
151
+ motion: "assemble"
152
+ }), _jsx("h1", {
153
+ children: title
154
+ }), lead && _jsx("p", {
155
+ className: "mdbase-connect-lead",
156
+ children: lead
157
+ }), status && status !== error && _jsx("p", {
158
+ className: "mdbase-connect-status",
159
+ role: "status",
160
+ children: status
161
+ }), error && _jsx("p", {
162
+ className: "mdbase-connect-error",
163
+ role: "alert",
164
+ children: error
165
+ }), detail && _jsxs("details", {
166
+ className: "mdbase-connect-detail",
167
+ children: [_jsx("summary", {
168
+ children: _i18n._(
169
+ /** i18n */
170
+ {
171
+ id: "URmyfc",
172
+ message: "Details"
173
+ })
174
+ }), _jsx("code", {
175
+ children: detail
176
+ })]
177
+ }), children, footnote && _jsx("p", {
178
+ className: "mdbase-connect-footnote",
179
+ children: footnote
180
+ })]
181
+ })
182
+ });
183
+ }
184
+ let state = "idle";
185
+ let startedAt = 0;
186
+ let fallback;
187
+ const listeners = new Set();
188
+ function publish(next) {
189
+ if (state === next) return;
190
+ state = next;
191
+ for (const listener of listeners) listener();
192
+ }
193
+ function subscribe(listener) {
194
+ listeners.add(listener);
195
+ return () => listeners.delete(listener);
196
+ }
197
+ /** Starts the entrance, or returns when the running one started; null once it is over. */
198
+ function beginLaunchEntrance() {
199
+ if (state === "done") return null;
200
+ if (state === "idle") {
201
+ startedAt = performance.now();
202
+ publish("playing");
203
+ // Frames stop in a background tab; the entrance must still end so the app is never held.
204
+ fallback = setTimeout(finishLaunchEntrance, (FLOCK_TIMING.end + 0.5) * 1000);
205
+ }
206
+ return startedAt;
207
+ }
208
+ function finishLaunchEntrance() {
209
+ clearTimeout(fallback);
210
+ publish("done");
211
+ }
212
+ /** Whether the launch entrance can play here: a real browser, motion allowed, page in view. */
213
+ function launchEntranceEligible() {
214
+ return typeof window !== "undefined" && typeof window.matchMedia === "function" && !window.matchMedia("(prefers-reduced-motion: reduce)").matches && !document.hidden;
215
+ }
216
+ /**
217
+ * True while the launch entrance is flying. An app that opens quickly keeps its
218
+ * opening screen until this is false, so the entrance is never cut off mid-flight;
219
+ * it lasts at most two seconds.
220
+ */
221
+ export function useLaunchEntrancePlaying() {
222
+ return useSyncExternalStore(subscribe, () => state === "playing", () => false);
223
+ }
224
+ /**
225
+ * Flies the flock onto the mark inside `target` and reports whether it is still flying.
226
+ * While it flies, the caller hides its real mark; the last frame is drawn from the same
227
+ * rectangles, so the hand-over is seamless.
228
+ */
229
+ function useFlockEntrance(target) {
230
+ const [eligible] = useState(() => state !== "done" && launchEntranceEligible());
231
+ const launch = useSyncExternalStore(subscribe, () => state, () => "idle");
232
+ const canvasRef = useRef(null);
233
+ const flying = eligible && launch !== "done";
234
+ useEffect(() => {
235
+ if (!eligible) {
236
+ // Nothing played here, and nothing should play later out of context.
237
+ if (state === "idle") publish("done");
238
+ return;
239
+ }
240
+ const started = beginLaunchEntrance();
241
+ const canvas = canvasRef.current;
242
+ const context = canvas?.getContext("2d");
243
+ if (started === null || !canvas || !context) return finishLaunchEntrance();
244
+ let frame = 0;
245
+ const draw = () => {
246
+ const t = (performance.now() - started) / 1000;
247
+ const mark = target.current?.querySelector("svg");
248
+ if (t >= FLOCK_TIMING.end || !mark) return finishLaunchEntrance();
249
+ const box = mark.getBoundingClientRect();
250
+ const ratio = Math.min(2, window.devicePixelRatio || 1);
251
+ const width = window.innerWidth;
252
+ const height = window.innerHeight;
253
+ if (canvas.width !== Math.round(width * ratio) || canvas.height !== Math.round(height * ratio)) {
254
+ canvas.width = Math.round(width * ratio);
255
+ canvas.height = Math.round(height * ratio);
256
+ }
257
+ // The mark's own colours, so app marks keep their inverted palette.
258
+ const ink = getComputedStyle(mark.querySelector(".mdbase-mark-ink") ?? mark).fill;
259
+ const accent = getComputedStyle(mark.querySelector(".mdbase-mark-accent") ?? mark).fill;
260
+ context.setTransform(ratio, 0, 0, ratio, 0, 0);
261
+ context.clearRect(0, 0, width, height);
262
+ const {
263
+ birds,
264
+ bars
265
+ } = flockFrame({
266
+ width,
267
+ height,
268
+ markLeft: box.left,
269
+ markTop: box.top,
270
+ markSize: Math.min(box.width, box.height)
271
+ }, t);
272
+ for (const rect of [...birds, ...bars]) {
273
+ context.fillStyle = rect.accent ? accent : ink;
274
+ context.fillRect(rect.x, rect.y, rect.width, rect.height);
275
+ }
276
+ frame = requestAnimationFrame(draw);
277
+ };
278
+ frame = requestAnimationFrame(draw);
279
+ return () => cancelAnimationFrame(frame);
280
+ }, [eligible, target]);
281
+ return {
282
+ flying,
283
+ canvas: flying ? _jsx("canvas", {
284
+ ref: canvasRef,
285
+ className: "mdbase-opening-flock",
286
+ "aria-hidden": "true"
287
+ }) : null
288
+ };
20
289
  }
@@ -1,53 +1,59 @@
1
1
  export function isOptionGroup(item) {
2
- return "options" in item;
2
+ return "options" in item;
3
3
  }
4
4
  /** Options in display order, each remembering its group. */
5
5
  export function flattenOptions(items) {
6
- return items.flatMap((item) => isOptionGroup(item) ? item.options.map((option) => ({ ...option, group: item.label })) : [item]);
6
+ return items.flatMap(item => isOptionGroup(item) ? item.options.map(option => ({
7
+ ...option,
8
+ group: item.label
9
+ })) : [item]);
7
10
  }
8
11
  /** The next enabled option from `from` in `direction`, stopping at the ends like a native list. */
9
12
  export function stepOption(options, from, direction) {
10
- for (let index = from + direction; index >= 0 && index < options.length; index += direction) {
11
- if (!options[index]?.disabled) {
12
- return index;
13
- }
13
+ for (let index = from + direction; index >= 0 && index < options.length; index += direction) {
14
+ if (!options[index]?.disabled) {
15
+ return index;
14
16
  }
15
- return from;
17
+ }
18
+ return from;
16
19
  }
17
20
  /** The first (or last) enabled option, or -1 when every option is disabled. */
18
21
  export function edgeOption(options, edge) {
19
- const start = edge === "first" ? -1 : options.length;
20
- const found = stepOption(options, start, edge === "first" ? 1 : -1);
21
- return found === start ? -1 : found;
22
+ const start = edge === "first" ? -1 : options.length;
23
+ const found = stepOption(options, start, edge === "first" ? 1 : -1);
24
+ return found === start ? -1 : found;
22
25
  }
23
26
  /**
24
27
  * Type-to-select: the next enabled option whose label starts with what was typed. Typing one
25
28
  * letter repeatedly cycles through the options that start with it, as a native select does.
26
29
  */
27
30
  export function typeaheadOption(options, typed, from) {
28
- const query = typed.toLocaleLowerCase();
29
- const prefix = /^(.)\1+$/su.test(query) ? query.slice(0, 1) : query;
30
- const offset = prefix.length === 1 ? 1 : 0;
31
- for (let step = 0; step < options.length; step += 1) {
32
- const index = (from + offset + step + options.length) % options.length;
33
- const option = options[index];
34
- if (option && !option.disabled && option.label.toLocaleLowerCase().startsWith(prefix)) {
35
- return index;
36
- }
31
+ const query = typed.toLocaleLowerCase();
32
+ const prefix = /^(.)\1+$/su.test(query) ? query.slice(0, 1) : query;
33
+ const offset = prefix.length === 1 ? 1 : 0;
34
+ for (let step = 0; step < options.length; step += 1) {
35
+ const index = (from + offset + step + options.length) % options.length;
36
+ const option = options[index];
37
+ if (option && !option.disabled && option.label.toLocaleLowerCase().startsWith(prefix)) {
38
+ return index;
37
39
  }
38
- return -1;
40
+ }
41
+ return -1;
39
42
  }
40
43
  /** Where each item's options start in the flattened list, so groups can render in place. */
41
44
  export function itemOffsets(items) {
42
- const offsets = [];
43
- let next = 0;
44
- for (const item of items) {
45
- offsets.push(next);
46
- next += isOptionGroup(item) ? item.options.length : 1;
47
- }
48
- return offsets;
45
+ const offsets = [];
46
+ let next = 0;
47
+ for (const item of items) {
48
+ offsets.push(next);
49
+ next += isOptionGroup(item) ? item.options.length : 1;
50
+ }
51
+ return offsets;
49
52
  }
50
53
  /** Appends a typed key, starting over after a pause, as native selects do. */
51
54
  export function extendTypeahead(buffer, key, at) {
52
- return { text: at - buffer.at > 600 ? key : buffer.text + key, at };
55
+ return {
56
+ text: at - buffer.at > 600 ? key : buffer.text + key,
57
+ at
58
+ };
53
59
  }
@@ -1,3 +1,4 @@
1
+ import "./i18n.js";
1
2
  export type DiffKind = "same" | "local" | "remote" | "omitted";
2
3
  export interface DiffLine {
3
4
  kind: DiffKind;
package/dist/text-diff.js CHANGED
@@ -1,89 +1,121 @@
1
+ import { i18n as _i18n } from "@lingui/core";
2
+ import "./i18n.js";
1
3
  const MAX_MATRIX_LINES = 240;
2
4
  const MAX_VISIBLE_CHANGES = 80;
3
5
  const CONTEXT_LINES = 3;
4
6
  export function compareLines(localText, remoteText) {
5
- const local = localText.split("\n");
6
- const remote = remoteText.split("\n");
7
- const compared = local.length <= MAX_MATRIX_LINES && remote.length <= MAX_MATRIX_LINES
8
- ? lcsDiff(local, remote)
9
- : boundedDiff(local, remote);
10
- return compactDiff(compared);
7
+ const local = localText.split("\n");
8
+ const remote = remoteText.split("\n");
9
+ const compared = local.length <= MAX_MATRIX_LINES && remote.length <= MAX_MATRIX_LINES ? lcsDiff(local, remote) : boundedDiff(local, remote);
10
+ return compactDiff(compared);
11
11
  }
12
12
  function lcsDiff(local, remote) {
13
- const lengths = Array.from({ length: local.length + 1 }, () => new Uint16Array(remote.length + 1));
14
- for (let localIndex = local.length - 1; localIndex >= 0; localIndex -= 1) {
15
- for (let remoteIndex = remote.length - 1; remoteIndex >= 0; remoteIndex -= 1) {
16
- lengths[localIndex][remoteIndex] = local[localIndex] === remote[remoteIndex]
17
- ? lengths[localIndex + 1][remoteIndex + 1] + 1
18
- : Math.max(lengths[localIndex + 1][remoteIndex], lengths[localIndex][remoteIndex + 1]);
19
- }
13
+ const lengths = Array.from({
14
+ length: local.length + 1
15
+ }, () => new Uint16Array(remote.length + 1));
16
+ for (let localIndex = local.length - 1; localIndex >= 0; localIndex -= 1) {
17
+ for (let remoteIndex = remote.length - 1; remoteIndex >= 0; remoteIndex -= 1) {
18
+ lengths[localIndex][remoteIndex] = local[localIndex] === remote[remoteIndex] ? lengths[localIndex + 1][remoteIndex + 1] + 1 : Math.max(lengths[localIndex + 1][remoteIndex], lengths[localIndex][remoteIndex + 1]);
20
19
  }
21
- const lines = [];
22
- let localIndex = 0;
23
- let remoteIndex = 0;
24
- while (localIndex < local.length || remoteIndex < remote.length) {
25
- if (localIndex < local.length && remoteIndex < remote.length && local[localIndex] === remote[remoteIndex]) {
26
- lines.push({ kind: "same", text: local[localIndex], localLine: localIndex + 1, remoteLine: remoteIndex + 1 });
27
- localIndex += 1;
28
- remoteIndex += 1;
29
- }
30
- else if (remoteIndex >= remote.length || (localIndex < local.length && lengths[localIndex + 1][remoteIndex] >= lengths[localIndex][remoteIndex + 1])) {
31
- lines.push({ kind: "local", text: local[localIndex], localLine: localIndex + 1 });
32
- localIndex += 1;
33
- }
34
- else {
35
- lines.push({ kind: "remote", text: remote[remoteIndex], remoteLine: remoteIndex + 1 });
36
- remoteIndex += 1;
37
- }
20
+ }
21
+ const lines = [];
22
+ let localIndex = 0;
23
+ let remoteIndex = 0;
24
+ while (localIndex < local.length || remoteIndex < remote.length) {
25
+ if (localIndex < local.length && remoteIndex < remote.length && local[localIndex] === remote[remoteIndex]) {
26
+ lines.push({
27
+ kind: "same",
28
+ text: local[localIndex],
29
+ localLine: localIndex + 1,
30
+ remoteLine: remoteIndex + 1
31
+ });
32
+ localIndex += 1;
33
+ remoteIndex += 1;
34
+ } else if (remoteIndex >= remote.length || localIndex < local.length && lengths[localIndex + 1][remoteIndex] >= lengths[localIndex][remoteIndex + 1]) {
35
+ lines.push({
36
+ kind: "local",
37
+ text: local[localIndex],
38
+ localLine: localIndex + 1
39
+ });
40
+ localIndex += 1;
41
+ } else {
42
+ lines.push({
43
+ kind: "remote",
44
+ text: remote[remoteIndex],
45
+ remoteLine: remoteIndex + 1
46
+ });
47
+ remoteIndex += 1;
38
48
  }
39
- return lines;
49
+ }
50
+ return lines;
40
51
  }
41
52
  function boundedDiff(local, remote) {
42
- let prefix = 0;
43
- while (prefix < local.length && prefix < remote.length && local[prefix] === remote[prefix])
44
- prefix += 1;
45
- let suffix = 0;
46
- while (suffix < local.length - prefix
47
- && suffix < remote.length - prefix
48
- && local[local.length - suffix - 1] === remote[remote.length - suffix - 1])
49
- suffix += 1;
50
- return [
51
- ...local.slice(0, prefix).map((text, index) => ({ kind: "same", text, localLine: index + 1, remoteLine: index + 1 })),
52
- ...local.slice(prefix, local.length - suffix).map((text, index) => ({ kind: "local", text, localLine: prefix + index + 1 })),
53
- ...remote.slice(prefix, remote.length - suffix).map((text, index) => ({ kind: "remote", text, remoteLine: prefix + index + 1 })),
54
- ...local.slice(local.length - suffix).map((text, index) => ({
55
- kind: "same",
56
- text,
57
- localLine: local.length - suffix + index + 1,
58
- remoteLine: remote.length - suffix + index + 1
59
- }))
60
- ];
53
+ let prefix = 0;
54
+ while (prefix < local.length && prefix < remote.length && local[prefix] === remote[prefix]) prefix += 1;
55
+ let suffix = 0;
56
+ while (suffix < local.length - prefix && suffix < remote.length - prefix && local[local.length - suffix - 1] === remote[remote.length - suffix - 1]) suffix += 1;
57
+ return [...local.slice(0, prefix).map((text, index) => ({
58
+ kind: "same",
59
+ text,
60
+ localLine: index + 1,
61
+ remoteLine: index + 1
62
+ })), ...local.slice(prefix, local.length - suffix).map((text, index) => ({
63
+ kind: "local",
64
+ text,
65
+ localLine: prefix + index + 1
66
+ })), ...remote.slice(prefix, remote.length - suffix).map((text, index) => ({
67
+ kind: "remote",
68
+ text,
69
+ remoteLine: prefix + index + 1
70
+ })), ...local.slice(local.length - suffix).map((text, index) => ({
71
+ kind: "same",
72
+ text,
73
+ localLine: local.length - suffix + index + 1,
74
+ remoteLine: remote.length - suffix + index + 1
75
+ }))];
61
76
  }
62
77
  function compactDiff(lines) {
63
- const changes = lines.reduce((count, line) => count + (line.kind === "same" ? 0 : 1), 0);
64
- if (changes === 0)
65
- return [];
66
- const visible = [];
67
- let shownChanges = 0;
68
- for (let index = 0; index < lines.length; index += 1) {
69
- const line = lines[index];
70
- if (line.kind !== "same") {
71
- if (shownChanges >= MAX_VISIBLE_CHANGES) {
72
- visible.push({ kind: "omitted", text: `${changes - shownChanges} more changed lines` });
73
- break;
78
+ const changes = lines.reduce((count, line) => count + (line.kind === "same" ? 0 : 1), 0);
79
+ if (changes === 0) return [];
80
+ const visible = [];
81
+ let shownChanges = 0;
82
+ for (let index = 0; index < lines.length; index += 1) {
83
+ const line = lines[index];
84
+ if (line.kind !== "same") {
85
+ if (shownChanges >= MAX_VISIBLE_CHANGES) {
86
+ visible.push({
87
+ kind: "omitted",
88
+ text: _i18n._(
89
+ /** i18n */
90
+ {
91
+ id: "ZZ4v5y",
92
+ message: "{0, plural, one {# more changed line} other {# more changed lines}}",
93
+ values: {
94
+ 0: changes - shownChanges
74
95
  }
75
- visible.push(line);
76
- shownChanges += 1;
77
- continue;
78
- }
79
- const previousChange = lines.slice(Math.max(0, index - CONTEXT_LINES), index).some((candidate) => candidate.kind !== "same");
80
- const nextChange = lines.slice(index + 1, index + CONTEXT_LINES + 1).some((candidate) => candidate.kind !== "same");
81
- if (previousChange || nextChange) {
82
- visible.push(line);
83
- }
84
- else if (visible.at(-1)?.kind !== "omitted") {
85
- visible.push({ kind: "omitted", text: "Unchanged lines" });
86
- }
96
+ })
97
+ });
98
+ break;
99
+ }
100
+ visible.push(line);
101
+ shownChanges += 1;
102
+ continue;
87
103
  }
88
- return visible;
104
+ const previousChange = lines.slice(Math.max(0, index - CONTEXT_LINES), index).some(candidate => candidate.kind !== "same");
105
+ const nextChange = lines.slice(index + 1, index + CONTEXT_LINES + 1).some(candidate => candidate.kind !== "same");
106
+ if (previousChange || nextChange) {
107
+ visible.push(line);
108
+ } else if (visible.at(-1)?.kind !== "omitted") {
109
+ visible.push({
110
+ kind: "omitted",
111
+ text: _i18n._(
112
+ /** i18n */
113
+ {
114
+ id: "_CVuTO",
115
+ message: "Unchanged lines"
116
+ })
117
+ });
118
+ }
119
+ }
120
+ return visible;
89
121
  }