flexdesk 0.17.18 → 0.18.1
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/CHANGELOG.md +91 -0
- package/README.md +1 -0
- package/css/flexdesk.css +7547 -5891
- package/css/reset.css +1 -1
- package/css/tokens.css +30 -1
- package/dist/charts.js +83 -13
- package/dist/charts.js.map +2 -2
- package/dist/chunk-3BLIVOFD.js +778 -0
- package/dist/chunk-3BLIVOFD.js.map +7 -0
- package/dist/{chunk-7OL6F2AM.js → chunk-4X4SY3CP.js} +7 -5
- package/dist/chunk-4X4SY3CP.js.map +7 -0
- package/dist/chunk-DNJ3N4T4.js +82 -0
- package/dist/chunk-DNJ3N4T4.js.map +7 -0
- package/dist/{chunk-PGZ4EHSE.js → chunk-HL3IIHU3.js} +501 -359
- package/dist/chunk-HL3IIHU3.js.map +7 -0
- package/dist/{chunk-REJ5HLIM.js → chunk-HYJYYEPK.js} +2 -2
- package/dist/{chunk-7U4IB65H.js → chunk-LLBJ2SJH.js} +5 -3
- package/dist/{chunk-7U4IB65H.js.map → chunk-LLBJ2SJH.js.map} +1 -1
- package/dist/{chunk-XJNADRGR.js → chunk-NFZJ6P42.js} +188 -96
- package/dist/chunk-NFZJ6P42.js.map +7 -0
- package/dist/chunk-PAI2ZUWV.js +260 -0
- package/dist/chunk-PAI2ZUWV.js.map +7 -0
- package/dist/chunk-Q7BALEPX.js +527 -0
- package/dist/chunk-Q7BALEPX.js.map +7 -0
- package/dist/{chunk-2Z2ENHR3.js → chunk-QRBOIUU3.js} +7 -8
- package/dist/chunk-QRBOIUU3.js.map +7 -0
- package/dist/chunk-QXAOTGCC.js +40 -0
- package/dist/chunk-QXAOTGCC.js.map +7 -0
- package/dist/chunk-UKP22RUG.js +219 -0
- package/dist/chunk-UKP22RUG.js.map +7 -0
- package/dist/chunk-VLOAI3HS.js +43 -0
- package/dist/chunk-VLOAI3HS.js.map +7 -0
- package/dist/{chunk-WUFLGETN.js → chunk-WVFGV5FT.js} +2 -39
- package/dist/{chunk-WUFLGETN.js.map → chunk-WVFGV5FT.js.map} +3 -3
- package/dist/editor.js +3 -2
- package/dist/editor.js.map +1 -1
- package/dist/flexdesk.css +7547 -5891
- package/dist/flow.js +29 -97
- package/dist/flow.js.map +3 -3
- package/dist/git.js +1356 -0
- package/dist/git.js.map +7 -0
- package/dist/reset.css +1 -1
- package/dist/tiles.js +20 -15
- package/dist/tiles.js.map +2 -2
- package/dist/tokens.css +30 -1
- package/dist/widgets.js +4146 -1199
- package/dist/widgets.js.map +4 -4
- package/dist/wm.js +120 -32
- package/dist/wm.js.map +2 -2
- package/docs/README.md +9 -4
- package/docs/best-practices.md +2 -2
- package/docs/components/charts.md +40 -0
- package/docs/components/chat-shell.md +204 -0
- package/docs/components/conversation.md +10 -0
- package/docs/components/filter-builder.md +292 -0
- package/docs/components/forms-and-dialogs.md +30 -0
- package/docs/components/git.md +355 -0
- package/docs/components/input-fields.md +191 -2
- package/docs/components/keymap.md +8 -0
- package/docs/components/menus.md +86 -2
- package/docs/components/panels-and-desktops.md +16 -5
- package/docs/components/shell.md +108 -1
- package/docs/components/workspace-picker.md +193 -0
- package/docs/concepts/building-and-packaging.md +2 -2
- package/docs/concepts/packages-and-entry-points.md +14 -13
- package/docs/concepts/state-guard.md +3 -3
- package/docs/quickstart.md +1 -1
- package/git.js +13 -0
- package/package.json +6 -1
- package/src/charts/plot_config_editor.js +92 -6
- package/src/charts/plot_grid.js +6 -1
- package/src/flow/kit/dom.js +28 -119
- package/src/flow/kit/step_picker.js +1 -1
- package/src/flow/kit/value_picker.js +1 -1
- package/src/flow/outline/menu.js +1 -1
- package/src/git/diff_viewer.js +212 -0
- package/src/git/diff_window.js +51 -0
- package/src/git/git_graph.js +401 -0
- package/src/git/git_ops.js +61 -0
- package/src/git/git_panel.js +755 -0
- package/src/git/index.js +9 -0
- package/src/git/line_diff.js +202 -0
- package/src/tiles/tile_base.js +5 -4
- package/src/tiling/desktops.js +10 -8
- package/src/tiling/keymap.js +6 -5
- package/src/tiling/kind_taxonomy.js +15 -2
- package/src/tiling/shell.js +124 -17
- package/src/tiling/tile_renderer.js +2 -3
- package/src/ui/components/action_dropdown.js +4 -78
- package/src/ui/components/anchored_layer.js +255 -0
- package/src/ui/components/chat_shell.js +926 -0
- package/src/ui/components/chat_state_machine.js +223 -0
- package/src/ui/components/choice_controls.js +45 -2
- package/src/ui/components/color_field.js +3 -2
- package/src/ui/components/color_list_field.js +3 -2
- package/src/ui/components/combobox.js +422 -0
- package/src/ui/components/context_menu.js +8 -2
- package/src/ui/components/conversation_view.js +17 -2
- package/src/ui/components/data_table.js +2 -2
- package/src/ui/components/filter_builder.js +791 -0
- package/src/ui/components/filter_model.js +682 -0
- package/src/ui/components/inline_renamer.js +10 -5
- package/src/ui/components/managed_window.js +24 -47
- package/src/ui/components/modal.js +104 -117
- package/src/ui/components/option_list.js +160 -0
- package/src/ui/components/popover.js +94 -0
- package/src/ui/components/recent_items.js +169 -0
- package/src/ui/components/select.js +322 -0
- package/src/ui/components/tag_input.js +77 -8
- package/src/ui/components/workspace_picker.js +1253 -0
- package/src/ui/utils/fire_event.js +17 -0
- package/src/ui/utils/focusable.js +25 -0
- package/src/ui/utils/modal_host.js +27 -0
- package/src/ui/utils/tooltip_service.js +81 -14
- package/widgets.js +12 -2
- package/css/base.css +0 -10547
- package/css/overrides.css +0 -2596
- package/dist/chunk-2Z2ENHR3.js.map +0 -7
- package/dist/chunk-7JI7HHSY.js +0 -107
- package/dist/chunk-7JI7HHSY.js.map +0 -7
- package/dist/chunk-7OL6F2AM.js.map +0 -7
- package/dist/chunk-MIW7D6WC.js +0 -526
- package/dist/chunk-MIW7D6WC.js.map +0 -7
- package/dist/chunk-PGZ4EHSE.js.map +0 -7
- package/dist/chunk-XJNADRGR.js.map +0 -7
- /package/dist/{chunk-REJ5HLIM.js.map → chunk-HYJYYEPK.js.map} +0 -0
|
@@ -28,6 +28,14 @@
|
|
|
28
28
|
* added. A 2D chart type removes the z axis (`zAxis: null`, as in
|
|
29
29
|
* makeDefaultSubplot).
|
|
30
30
|
*
|
|
31
|
+
* `hide` takes parts of the editor away, for an embedder whose plots never use
|
|
32
|
+
* them: '3d' (the 3D chart types and the z axis), 'stackGroup' (a series' stack
|
|
33
|
+
* group), 'xAxis' (the x axis section; the subplot then plots against time).
|
|
34
|
+
*
|
|
35
|
+
* A reference line's value is a number. With `constantsProvider` it may also name
|
|
36
|
+
* a constant: the box offers the names, and stores a name as a string, which
|
|
37
|
+
* renderPlotGrid's `resolveConstant` turns into a value at each draw.
|
|
38
|
+
*
|
|
31
39
|
* Series names come from `seriesProvider`, through an AutocompleteField per
|
|
32
40
|
* picker: it is asked `{ value, role, subplot }` (`role` is 'series', 'x' or
|
|
33
41
|
* 'z') and answers AutocompleteField items (`{ label, insertText?, type? }`)
|
|
@@ -43,6 +51,7 @@
|
|
|
43
51
|
*/
|
|
44
52
|
|
|
45
53
|
import { AutocompleteField } from '../ui/components/autocomplete_field.js';
|
|
54
|
+
import { attachInputOptions } from '../ui/components/combobox.js';
|
|
46
55
|
import { ColorField } from '../ui/components/color_field.js';
|
|
47
56
|
import { ToggleField } from '../ui/components/toggle_field.js';
|
|
48
57
|
import { selectBox } from '../ui/components/choice_controls.js';
|
|
@@ -55,6 +64,7 @@ const CHART_TYPE_OPTIONS = Object.freeze([
|
|
|
55
64
|
{ group: '2D', options: CHART_TYPES_2D },
|
|
56
65
|
{ group: '3D', options: CHART_TYPES_3D },
|
|
57
66
|
]);
|
|
67
|
+
const HIDEABLE = Object.freeze(['3d', 'stackGroup', 'xAxis']);
|
|
58
68
|
const SCALES = Object.freeze([{ value: 'linear', label: 'Linear' }, { value: 'log', label: 'Log' }]);
|
|
59
69
|
const POSITIONS = Object.freeze([{ value: 'left', label: 'Left' }, { value: 'right', label: 'Right' }]);
|
|
60
70
|
const DASHES = Object.freeze([
|
|
@@ -200,6 +210,10 @@ let editorCount = 0;
|
|
|
200
210
|
* or return an ARRAY of field specs `{ label: string, control: HTMLElement,
|
|
201
211
|
* hint? }` / `{ label: string, render(slot) => { dispose? } | void, hint? }`,
|
|
202
212
|
* laid out after anything drawn into `el`. A bad spec is skipped with a warning.
|
|
213
|
+
* @param {Array<'3d'|'stackGroup'|'xAxis'>} [options.hide] - Parts of the editor not to
|
|
214
|
+
* draw. See the file's header. Any other name throws.
|
|
215
|
+
* @param {Function} [options.constantsProvider] - `() => [{ name, value, label?, group? }]`:
|
|
216
|
+
* the names a reference line's value may take, asked each time the list is drawn.
|
|
203
217
|
* @param {Object} [options.logger] - Given to every AutocompleteField (provider errors);
|
|
204
218
|
* its `warn` also hears a part (an `extraSeriesFields` handle, say) that throws on dispose.
|
|
205
219
|
* @returns {{ dispose: Function, refresh: Function, selectSubplot: Function,
|
|
@@ -212,9 +226,15 @@ function renderPlotConfigEditor(container, config, options = {}) {
|
|
|
212
226
|
overlaySources: null,
|
|
213
227
|
seriesTransforms: null,
|
|
214
228
|
extraSeriesFields: null,
|
|
229
|
+
hide: [],
|
|
230
|
+
constantsProvider: null,
|
|
215
231
|
logger: null,
|
|
216
232
|
...options,
|
|
217
233
|
};
|
|
234
|
+
for (const part of opts.hide) {
|
|
235
|
+
if (!HIDEABLE.includes(part)) throw new TypeError(`[plot config editor] cannot hide "${part}"`);
|
|
236
|
+
}
|
|
237
|
+
const hidden = new Set(opts.hide);
|
|
218
238
|
const editorId = `twm-plot-config-${++editorCount}`;
|
|
219
239
|
let fieldCount = 0;
|
|
220
240
|
let active = 0;
|
|
@@ -380,6 +400,7 @@ function renderPlotConfigEditor(container, config, options = {}) {
|
|
|
380
400
|
|
|
381
401
|
function normalise(sp) {
|
|
382
402
|
if (!sp.xAxis) sp.xAxis = { useTime: true, variable: '' };
|
|
403
|
+
if (hidden.has('xAxis')) sp.xAxis.useTime = true;
|
|
383
404
|
if (!Array.isArray(sp.yAxes) || !sp.yAxes.length) sp.yAxes = [makeDefaultYAxis(0)];
|
|
384
405
|
for (const axis of sp.yAxes) if (!Array.isArray(axis.series)) axis.series = [];
|
|
385
406
|
if (is3DChart(sp.chartType) && !sp.zAxis) sp.zAxis = makeZAxis();
|
|
@@ -403,9 +424,9 @@ function renderPlotConfigEditor(container, config, options = {}) {
|
|
|
403
424
|
section('Subplot', field('Title', textInput(sp.displayName, (v) => { sp.displayName = v; notify(); },
|
|
404
425
|
{ placeholder: 'Untitled' }))),
|
|
405
426
|
section('Display', ...displayFields(sp)),
|
|
406
|
-
section('X axis', ...xAxisFields(sp)),
|
|
427
|
+
hidden.has('xAxis') ? null : section('X axis', ...xAxisFields(sp)),
|
|
407
428
|
section('Y axes', yAxesEditor(sp, is3D, overlaySources)),
|
|
408
|
-
is3D ? section('Z axis', ...zAxisFields(sp)) : null,
|
|
429
|
+
is3D && !hidden.has('3d') ? section('Z axis', ...zAxisFields(sp)) : null,
|
|
409
430
|
section('Reference lines', referenceLinesEditor(sp)),
|
|
410
431
|
section('Shaded bands', shadedBandsEditor(sp)),
|
|
411
432
|
transformsSection(sp),
|
|
@@ -414,7 +435,7 @@ function renderPlotConfigEditor(container, config, options = {}) {
|
|
|
414
435
|
|
|
415
436
|
function displayFields(sp) {
|
|
416
437
|
return [
|
|
417
|
-
field('Chart type', select(CHART_TYPE_OPTIONS, sp.chartType ?? 'line', (v) => {
|
|
438
|
+
field('Chart type', select(hidden.has('3d') ? CHART_TYPES_2D : CHART_TYPE_OPTIONS, sp.chartType ?? 'line', (v) => {
|
|
418
439
|
const was3D = is3DChart(sp.chartType);
|
|
419
440
|
sp.chartType = v;
|
|
420
441
|
const now3D = is3DChart(v);
|
|
@@ -543,9 +564,11 @@ function renderPlotConfigEditor(container, config, options = {}) {
|
|
|
543
564
|
field('Width', numberInput(s.lineWidth ?? 2, (v) => { s.lineWidth = v ?? 2; notify(); },
|
|
544
565
|
{ placeholder: '2', label: 'Line width', min: 1, max: 10, step: 1 })),
|
|
545
566
|
field('Dash', select(DASHES, s.lineStyle ?? 'solid', (v) => { s.lineStyle = v; notify(); }, { label: 'Dash' })),
|
|
546
|
-
field('Stack group', textInput(s.stackGroup, (v) => { s.stackGroup = v || null; notify(); },
|
|
547
|
-
{ placeholder: 'None', label: 'Stack group' })),
|
|
548
567
|
);
|
|
568
|
+
if (!hidden.has('stackGroup')) {
|
|
569
|
+
fields.appendChild(field('Stack group', textInput(s.stackGroup, (v) => { s.stackGroup = v || null; notify(); },
|
|
570
|
+
{ placeholder: 'None', label: 'Stack group' })));
|
|
571
|
+
}
|
|
549
572
|
if (opts.extraSeriesFields) fields.appendChild(extraFields(sp, axis, s));
|
|
550
573
|
if (opts.overlaySources) fields.appendChild(overlayEditor(s, overlaySources));
|
|
551
574
|
row.append(head, fields);
|
|
@@ -655,6 +678,67 @@ function renderPlotConfigEditor(container, config, options = {}) {
|
|
|
655
678
|
return wrap;
|
|
656
679
|
}
|
|
657
680
|
|
|
681
|
+
/**
|
|
682
|
+
* A reference line's value as a number or a constant's name. What was typed
|
|
683
|
+
* settles on change, on Enter or Tab, or on a pick; a typed number stays a number
|
|
684
|
+
* even when it is part of a constant's name, so Enter does not take that row.
|
|
685
|
+
*/
|
|
686
|
+
function constantValueField(line) {
|
|
687
|
+
const wrap = el('div', 'twm-plot-config__constant');
|
|
688
|
+
const input = el('input', 'twm-plot-config__input');
|
|
689
|
+
input.type = 'text';
|
|
690
|
+
input.spellcheck = false;
|
|
691
|
+
input.placeholder = 'Number or name';
|
|
692
|
+
input.setAttribute('aria-label', 'Value');
|
|
693
|
+
input.value = line.value ?? '';
|
|
694
|
+
const now = el('span', 'twm-plot-config__constant-value');
|
|
695
|
+
wrap.append(input, now);
|
|
696
|
+
|
|
697
|
+
const isNumber = (text) => text !== '' && Number.isFinite(Number(text));
|
|
698
|
+
const sayValue = () => {
|
|
699
|
+
const named = opts.constantsProvider().find((c) => c.name === input.value.trim());
|
|
700
|
+
now.textContent = named ? `= ${named.value}` : '';
|
|
701
|
+
};
|
|
702
|
+
const settle = () => {
|
|
703
|
+
const text = input.value.trim();
|
|
704
|
+
const next = isNumber(text) ? Number(text) : text || null;
|
|
705
|
+
sayValue();
|
|
706
|
+
if (next === (line.value ?? null)) return;
|
|
707
|
+
line.value = next;
|
|
708
|
+
notify();
|
|
709
|
+
};
|
|
710
|
+
const offered = (query) => {
|
|
711
|
+
const words = (query ?? '').trim().toLowerCase();
|
|
712
|
+
const groups = new Map();
|
|
713
|
+
for (const c of opts.constantsProvider()) {
|
|
714
|
+
const text = c.label ?? c.name;
|
|
715
|
+
if (words && !`${c.name} ${text}`.toLowerCase().includes(words)) continue;
|
|
716
|
+
if (!groups.has(c.group)) groups.set(c.group, []);
|
|
717
|
+
groups.get(c.group).push({ value: c.name, label: text, hint: `= ${c.value}` });
|
|
718
|
+
}
|
|
719
|
+
return [...groups].flatMap(([group, rows]) => (group ? [{ group, options: rows }] : rows));
|
|
720
|
+
};
|
|
721
|
+
const list = attachInputOptions(input, {
|
|
722
|
+
options: offered,
|
|
723
|
+
debounce: 0,
|
|
724
|
+
autoHighlight: true,
|
|
725
|
+
isCandidate: (typed) => isNumber(typed.trim()),
|
|
726
|
+
openOnFocus: true,
|
|
727
|
+
isCurrent: (option) => option.value === input.value.trim(),
|
|
728
|
+
onPick: (option) => {
|
|
729
|
+
input.value = option.value;
|
|
730
|
+
settle();
|
|
731
|
+
},
|
|
732
|
+
onSettle: settle,
|
|
733
|
+
});
|
|
734
|
+
input.addEventListener('change', settle);
|
|
735
|
+
// After the list's own keys, which take Enter while a row is active.
|
|
736
|
+
input.addEventListener('keydown', (event) => { if (event.key === 'Enter') settle(); });
|
|
737
|
+
track(() => list.destroy());
|
|
738
|
+
sayValue();
|
|
739
|
+
return wrap;
|
|
740
|
+
}
|
|
741
|
+
|
|
658
742
|
function referenceLinesEditor(sp) {
|
|
659
743
|
const axisOptions = sp.yAxes.map((a, i) => ({ value: a.id, label: a.label || `Y axis ${i + 1}` }));
|
|
660
744
|
return listEditor(sp, 'referenceLines', {
|
|
@@ -663,7 +747,9 @@ function renderPlotConfigEditor(container, config, options = {}) {
|
|
|
663
747
|
fields: (line) => [
|
|
664
748
|
select(axisOptions, line.yAxisId ?? sp.yAxes[0].id, (v) => { line.yAxisId = v; notify(); },
|
|
665
749
|
{ label: 'Axis' }),
|
|
666
|
-
|
|
750
|
+
opts.constantsProvider
|
|
751
|
+
? constantValueField(line)
|
|
752
|
+
: numberInput(line.value, (v) => { line.value = v; notify(); }, { placeholder: 'Value', label: 'Value' }),
|
|
667
753
|
textInput(line.label, (v) => { line.label = v; notify(); }, { placeholder: 'Label', label: 'Label' }),
|
|
668
754
|
colorField(line.color, (v) => { if (v) line.color = v; else delete line.color; }, 'Colour'),
|
|
669
755
|
select(DASHES, line.lineStyle ?? 'dashed', (v) => { line.lineStyle = v; notify(); }, { label: 'Dash' }),
|
package/src/charts/plot_grid.js
CHANGED
|
@@ -91,6 +91,9 @@ function emptyMessageFor(message, subplot, results, index) {
|
|
|
91
91
|
* @param {Object} [options]
|
|
92
92
|
* @param {Function} [options.resolveSeries] - `(series, results, subplot) => { y, x? } | null`;
|
|
93
93
|
* default resolveSeriesData. Also resolves the x variable, as `{ variable }`.
|
|
94
|
+
* @param {Function} [options.resolveConstant] - `(name) => number | undefined`: the value of a
|
|
95
|
+
* reference line that stores a name (renderPlotConfigEditor's `constantsProvider`),
|
|
96
|
+
* asked at each draw. A name it does not know draws no line; so does any name without it.
|
|
94
97
|
* @param {Object} [options.overlays] - `{ [series.id]: { years, values } }`, drawn for
|
|
95
98
|
* every series with an `overlay` description.
|
|
96
99
|
* @param {Object} [options.seriesTransforms] - `{ [id]: transform }` (series_transforms.js).
|
|
@@ -326,7 +329,9 @@ function renderPlotGrid(container, plotConfig, results, options = {}) {
|
|
|
326
329
|
? 'top' : (sp.legendPosition ?? 'hidden');
|
|
327
330
|
const xRange = !useX && time.length >= 2 ? [time[0], time[time.length - 1]] : undefined;
|
|
328
331
|
const { shapes, annotations } = buildOverlayShapes({
|
|
329
|
-
referenceLines: sp.referenceLines
|
|
332
|
+
referenceLines: (sp.referenceLines ?? []).map((line) => (typeof line.value === 'string'
|
|
333
|
+
? { ...line, value: opts.resolveConstant?.(line.value) }
|
|
334
|
+
: line)),
|
|
330
335
|
shadedBands: sp.shadedBands,
|
|
331
336
|
yRefByAxisId,
|
|
332
337
|
});
|
package/src/flow/kit/dom.js
CHANGED
|
@@ -1,27 +1,16 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* THE KIT'S SMALL DOM HELPERS, AND ITS
|
|
2
|
+
* THE KIT'S SMALL DOM HELPERS, AND ITS POPOVER.
|
|
3
3
|
*
|
|
4
4
|
* Everything here builds elements; nothing here reads the network, storage or
|
|
5
|
-
* a consumer's vocabulary. The popover
|
|
6
|
-
*
|
|
7
|
-
*
|
|
8
|
-
*
|
|
9
|
-
*
|
|
10
|
-
*
|
|
11
|
-
* - it is `position: fixed`, placed under its anchor (above when there is no
|
|
12
|
-
* room below) and CLAMPED to the viewport;
|
|
13
|
-
* - it is dismissed by a press outside it, by Escape, or by `close()`, and
|
|
14
|
-
* **it returns focus to its anchor** when it closes with focus inside it —
|
|
15
|
-
* a picker that drops focus on `<body>` drops it where an editor's keys
|
|
16
|
-
* never arrive — except when focus has just left it by Tab, which the
|
|
17
|
-
* pickers close it for (`'blur'`): that focus is going where it was sent;
|
|
18
|
-
* - there is ONE at a time: opening a second closes the first, and opening one
|
|
19
|
-
* from the anchor that holds the open one closes it (a toggle);
|
|
20
|
-
* - `closeFlowPopovers()` closes whichever is open, which is what an editor's
|
|
21
|
-
* `destroy()` calls so that nothing it opened outlives it.
|
|
5
|
+
* a consumer's vocabulary. The popover, the step picker's and *Insert a value*'s
|
|
6
|
+
* shell (36 §3.5, §3.7), is FlexDesk's `openPopover`, which says how it is placed,
|
|
7
|
+
* dismissed and toggled and that it returns focus to its anchor: a picker that
|
|
8
|
+
* drops focus on `<body>` drops it where an editor's keys never arrive. What the
|
|
9
|
+
* kit adds is `closeFlowPopovers()`, which closes whichever of its own is open:
|
|
10
|
+
* what an editor's `destroy()` calls so that nothing it opened outlives it.
|
|
22
11
|
*/
|
|
23
12
|
|
|
24
|
-
import {
|
|
13
|
+
import { openPopover as openAnchoredPopover } from '../../ui/components/popover.js';
|
|
25
14
|
|
|
26
15
|
/** The category tones (36 §3.1, §3.12); the stylesheet maps each to tokens. */
|
|
27
16
|
export const TONES = Object.freeze(['violet', 'teal', 'amber', 'grey', 'blue', 'indigo']);
|
|
@@ -81,118 +70,38 @@ export function uid(stem = 'flow') {
|
|
|
81
70
|
|
|
82
71
|
// ── the popover ──────────────────────────────────────────────────────────
|
|
83
72
|
|
|
84
|
-
|
|
85
|
-
const
|
|
86
|
-
let ACTIVE = null;
|
|
73
|
+
/** The kit's open popovers, oldest first. */
|
|
74
|
+
const OPEN = [];
|
|
87
75
|
|
|
88
|
-
/** Close
|
|
76
|
+
/** Close every kit popover that is open. */
|
|
89
77
|
export function closeFlowPopovers() {
|
|
90
|
-
|
|
78
|
+
OPEN[0]?.close('closed');
|
|
91
79
|
}
|
|
92
80
|
|
|
93
|
-
/** The open popover's element, or null — for a test. */
|
|
81
|
+
/** The open popover's element (the innermost's), or null — for a test. */
|
|
94
82
|
export function openFlowPopover() {
|
|
95
|
-
return
|
|
83
|
+
return OPEN.length ? OPEN[OPEN.length - 1].el : null;
|
|
96
84
|
}
|
|
97
85
|
|
|
98
86
|
/**
|
|
99
|
-
*
|
|
87
|
+
* FlexDesk's popover (`openPopover` in `ui/components/popover.js`), wearing the
|
|
88
|
+
* kit's class and remembered so that an editor can close what it opened.
|
|
100
89
|
*
|
|
101
|
-
* @param {object} o
|
|
102
|
-
* @
|
|
103
|
-
*
|
|
104
|
-
*
|
|
105
|
-
* @param {string} [o.className] an extra block class
|
|
106
|
-
* @param {(reason: string) => void} [o.onClose] after it is gone
|
|
107
|
-
* @param {HTMLElement} [o.focus] what takes focus on open
|
|
108
|
-
* @returns {{el: HTMLElement, close: (reason?: string) => void, reposition: () => void, toggled: boolean}}
|
|
109
|
-
* `toggled` is true when the call only CLOSED the popover its anchor held.
|
|
90
|
+
* @param {object} o What `openPopover` takes.
|
|
91
|
+
* @returns {{el: HTMLElement|null, close: (reason?: string) => void, reposition: () => void, toggled: boolean}}
|
|
92
|
+
* `el` is null when nothing opened; `toggled` is true when the call only
|
|
93
|
+
* CLOSED the popover its anchor held.
|
|
110
94
|
*/
|
|
111
|
-
export function openPopover({
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
const pop = el('div', `twm-flow-popover${className ? ` ${className}` : ''}`);
|
|
119
|
-
pop.setAttribute('role', 'dialog');
|
|
120
|
-
pop.setAttribute('aria-label', label || '');
|
|
121
|
-
pop.tabIndex = -1;
|
|
122
|
-
pop.appendChild(content);
|
|
123
|
-
(modalHost() || document.body).appendChild(pop);
|
|
124
|
-
|
|
125
|
-
let closed = false;
|
|
126
|
-
const place = () => {
|
|
127
|
-
if (closed) return;
|
|
128
|
-
// The CLIENT width: a page scrollbar is not room to draw in.
|
|
129
|
-
const view = { w: document.documentElement.clientWidth || window.innerWidth || 1024,
|
|
130
|
-
h: document.documentElement.clientHeight || window.innerHeight || 768 };
|
|
131
|
-
const a = anchor?.getBoundingClientRect?.() || { left: 0, top: 0, bottom: 0, right: 0 };
|
|
132
|
-
const box = pop.getBoundingClientRect();
|
|
133
|
-
// Under the anchor, its left edge on the anchor's; an anchor near the
|
|
134
|
-
// right edge gets the popover's RIGHT edge on its own instead.
|
|
135
|
-
let left = a.left;
|
|
136
|
-
if (left + box.width > view.w - EDGE && a.right - box.width >= EDGE) left = a.right - box.width;
|
|
137
|
-
let top = a.bottom + GAP;
|
|
138
|
-
if (top + box.height > view.h - EDGE && a.top - GAP - box.height >= EDGE) top = a.top - GAP - box.height;
|
|
139
|
-
left = Math.max(EDGE, Math.min(left, view.w - box.width - EDGE));
|
|
140
|
-
top = Math.max(EDGE, Math.min(top, view.h - box.height - EDGE));
|
|
141
|
-
pop.style.left = `${Math.round(left)}px`;
|
|
142
|
-
pop.style.top = `${Math.round(top)}px`;
|
|
143
|
-
};
|
|
144
|
-
|
|
145
|
-
const onPress = (ev) => {
|
|
146
|
-
if (pop.contains(ev.target)) return;
|
|
147
|
-
if (anchor && anchor.contains?.(ev.target)) return; // the anchor's own click toggles
|
|
148
|
-
handle.close('outside');
|
|
149
|
-
};
|
|
150
|
-
const onKey = (ev) => {
|
|
151
|
-
if (ev.key !== 'Escape') return;
|
|
152
|
-
ev.preventDefault();
|
|
153
|
-
ev.stopPropagation();
|
|
154
|
-
handle.close('escape');
|
|
155
|
-
};
|
|
156
|
-
const onResize = () => place();
|
|
157
|
-
// Placed before its content is drawn — a picker fills its list after this
|
|
158
|
-
// returns — so it is placed again whenever its size changes, or it would
|
|
159
|
-
// run past the window's edge.
|
|
160
|
-
const sized = typeof ResizeObserver === 'function' ? new ResizeObserver(() => place()) : null;
|
|
161
|
-
|
|
162
|
-
const handle = {
|
|
163
|
-
el: pop,
|
|
164
|
-
anchor,
|
|
165
|
-
toggled: false,
|
|
166
|
-
reposition: place,
|
|
167
|
-
close(reason = 'closed') {
|
|
168
|
-
if (closed) return;
|
|
169
|
-
closed = true;
|
|
170
|
-
const held = pop.contains(document.activeElement) || document.activeElement === document.body;
|
|
171
|
-
document.removeEventListener('pointerdown', onPress, true);
|
|
172
|
-
document.removeEventListener('mousedown', onPress, true);
|
|
173
|
-
pop.removeEventListener('keydown', onKey);
|
|
174
|
-
window.removeEventListener('resize', onResize);
|
|
175
|
-
sized?.disconnect();
|
|
176
|
-
pop.remove();
|
|
177
|
-
if (ACTIVE === handle) ACTIVE = null;
|
|
178
|
-
// Focus that LEFT the popover (a Tab out of it) is going somewhere the
|
|
179
|
-
// reader sent it; taking it back to the anchor would fight that move.
|
|
180
|
-
if (held && reason !== 'blur' && anchor?.isConnected) anchor.focus?.({ preventScroll: true });
|
|
95
|
+
export function openPopover({ className = '', onClose = null, ...rest }) {
|
|
96
|
+
const handle = openAnchoredPopover({
|
|
97
|
+
...rest,
|
|
98
|
+
className: `twm-flow-popover${className ? ` ${className}` : ''}`,
|
|
99
|
+
onClose(reason) {
|
|
100
|
+
OPEN.splice(OPEN.indexOf(handle), 1);
|
|
181
101
|
onClose?.(reason);
|
|
182
102
|
},
|
|
183
|
-
};
|
|
184
|
-
|
|
185
|
-
// The press that OPENED it is still being dispatched; listen from the next turn.
|
|
186
|
-
setTimeout(() => {
|
|
187
|
-
if (closed) return;
|
|
188
|
-
document.addEventListener('pointerdown', onPress, true);
|
|
189
|
-
document.addEventListener('mousedown', onPress, true);
|
|
190
|
-
}, 0);
|
|
191
|
-
pop.addEventListener('keydown', onKey);
|
|
192
|
-
window.addEventListener('resize', onResize);
|
|
193
|
-
sized?.observe(pop);
|
|
194
|
-
place();
|
|
195
|
-
(focus || pop).focus?.({ preventScroll: true });
|
|
103
|
+
});
|
|
104
|
+
if (handle.el) OPEN.push(handle);
|
|
196
105
|
return handle;
|
|
197
106
|
}
|
|
198
107
|
|
|
@@ -202,7 +202,7 @@ export function openStepPicker({ anchor, entries = [], categories = [], where =
|
|
|
202
202
|
anchor, content: root, label: title ?? say(S, 'addStep'), className: 'twm-flow-popover--picker',
|
|
203
203
|
focus: search, onClose: () => settle(result),
|
|
204
204
|
});
|
|
205
|
-
if (handle.
|
|
205
|
+
if (!handle.el) settle(null);
|
|
206
206
|
else filter();
|
|
207
207
|
done.close = () => handle.close('closed');
|
|
208
208
|
return done;
|
|
@@ -231,7 +231,7 @@ export function openValuePicker({ anchor, groups = [], note = null, onPick = nul
|
|
|
231
231
|
anchor, content: root, label: title ?? say(S, 'insertValue'), className: 'twm-flow-popover--values',
|
|
232
232
|
focus: search, onClose: () => settle(result),
|
|
233
233
|
});
|
|
234
|
-
if (handle.
|
|
234
|
+
if (!handle.el) settle(null);
|
|
235
235
|
else paint();
|
|
236
236
|
done.close = () => handle.close('closed');
|
|
237
237
|
return done;
|
package/src/flow/outline/menu.js
CHANGED
|
@@ -150,5 +150,5 @@ export function openStepMenu({ anchor, label, items, strings = null, onClose = n
|
|
|
150
150
|
anchor, content: root, label: label || '', className: 'twm-flow-popover--menu',
|
|
151
151
|
focus: enabled(root)[0] || root, onClose: () => onClose?.(),
|
|
152
152
|
});
|
|
153
|
-
return { el: root, close: () => handle.close('closed'), toggled: handle.toggled };
|
|
153
|
+
return { el: handle.el ? root : null, close: () => handle.close('closed'), toggled: handle.toggled };
|
|
154
154
|
}
|
|
@@ -0,0 +1,212 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* diff_viewer.js — what changed, drawn three ways. Usable alone: it needs no
|
|
3
|
+
* adapter and knows nothing of git beyond reading a unified diff.
|
|
4
|
+
*
|
|
5
|
+
* FIELDS named values, `Priority low → high`. The consumer parses them;
|
|
6
|
+
* they arrive as `{ name, from, to }`
|
|
7
|
+
* TEXT line by line, with old and new line numbers and no diff headers,
|
|
8
|
+
* from `before`/`after` (compared here) or else from `diff` (read)
|
|
9
|
+
* RAW the `diff` text as it came
|
|
10
|
+
*
|
|
11
|
+
* const viewer = createDiffViewer(el, { before, after, diff, fields });
|
|
12
|
+
* viewer.update({ before, after });
|
|
13
|
+
* viewer.setView('raw');
|
|
14
|
+
* viewer.destroy();
|
|
15
|
+
*
|
|
16
|
+
* A view is offered only when its data is there, and the buttons that switch
|
|
17
|
+
* between views only when there are two or more. Which view shows: the one set
|
|
18
|
+
* by `view`, `setView` or a click while it stays on offer, else Fields when a
|
|
19
|
+
* field changed, else Text, else Raw. `setView(null)` hands the choice back.
|
|
20
|
+
*/
|
|
21
|
+
|
|
22
|
+
import { computeLineDiff, parseUnifiedDiff } from './line_diff.js';
|
|
23
|
+
|
|
24
|
+
const DEFAULT_LABELS = Object.freeze({
|
|
25
|
+
fields: 'Fields',
|
|
26
|
+
text: 'Text',
|
|
27
|
+
raw: 'Raw',
|
|
28
|
+
noChanges: 'No differences.',
|
|
29
|
+
noFields: 'No field changes.',
|
|
30
|
+
tooLong: 'Too long to compare here.',
|
|
31
|
+
gap: (hidden) => `${hidden} unchanged line${hidden === 1 ? '' : 's'}`,
|
|
32
|
+
});
|
|
33
|
+
|
|
34
|
+
function el(tag, className, text) {
|
|
35
|
+
const n = document.createElement(tag);
|
|
36
|
+
if (className) n.className = className;
|
|
37
|
+
if (text !== undefined) n.textContent = text;
|
|
38
|
+
return n;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
/** A field's value as drawn: an absent or empty one reads as a dash. */
|
|
42
|
+
const shownField = (v) => {
|
|
43
|
+
const s = v === null || v === undefined ? '' : String(v);
|
|
44
|
+
return s === '' ? '—' : s;
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
function lineRow(line, labels) {
|
|
48
|
+
const row = el('div', `twm-diff__line twm-diff__line--${line.kind}`);
|
|
49
|
+
if (line.kind === 'gap' || line.kind === 'hunk' || line.kind === 'meta' || line.kind === 'file') {
|
|
50
|
+
row.textContent = line.kind === 'gap' ? labels.gap(line.hidden) : line.text;
|
|
51
|
+
return row;
|
|
52
|
+
}
|
|
53
|
+
row.append(
|
|
54
|
+
el('span', 'twm-diff__n', line.oldLine === null ? '' : String(line.oldLine)),
|
|
55
|
+
el('span', 'twm-diff__n', line.newLine === null ? '' : String(line.newLine)),
|
|
56
|
+
el('span', 'twm-diff__sign', line.kind === 'add' ? '+' : line.kind === 'del' ? '-' : ''),
|
|
57
|
+
el('span', 'twm-diff__text', line.text),
|
|
58
|
+
);
|
|
59
|
+
return row;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* @param {HTMLElement} host
|
|
64
|
+
* @param {Object} [options]
|
|
65
|
+
* @param {string} [options.before] - The old text. With `after`, drawn by comparing the two.
|
|
66
|
+
* @param {string} [options.after] - The new text.
|
|
67
|
+
* @param {string} [options.diff] - A unified diff: the Raw view, and the Text view when there is no `before`/`after`.
|
|
68
|
+
* @param {Array<{name: string, from?: *, to?: *}>} [options.fields] - The Fields view's rows.
|
|
69
|
+
* @param {number} [options.beforeStart=1] - The line number of `before`'s first line.
|
|
70
|
+
* @param {number} [options.afterStart=1] - The line number of `after`'s first line.
|
|
71
|
+
* @param {number} [options.context=3] - Unchanged lines kept around each change.
|
|
72
|
+
* @param {number} [options.maxLines] - Past this many differing lines a side, the texts are not compared.
|
|
73
|
+
* @param {'fields'|'text'|'raw'} [options.view] - The view to show while it is on offer.
|
|
74
|
+
* @param {Object} [options.labels] - Words to replace: `fields`, `text`, `raw`, `noChanges`, `noFields`, `tooLong`, `gap(hidden)`.
|
|
75
|
+
* @param {(view: string) => void} [options.onViewChange] - Told when a click changes the view.
|
|
76
|
+
* @returns {{ el: HTMLElement, update(data: Object): void, setView(view: string|null): void,
|
|
77
|
+
* view(): string|null, showMessage(text: string): void, destroy(): void }}
|
|
78
|
+
*/
|
|
79
|
+
export function createDiffViewer(host, options = {}) {
|
|
80
|
+
const { context = 3, maxLines, onViewChange = null } = options;
|
|
81
|
+
const labels = { ...DEFAULT_LABELS, ...options.labels };
|
|
82
|
+
|
|
83
|
+
const root = el('div', 'twm-diff');
|
|
84
|
+
const switcher = el('div', 'twm-diff__views');
|
|
85
|
+
switcher.setAttribute('role', 'group');
|
|
86
|
+
switcher.setAttribute('aria-label', 'Diff view');
|
|
87
|
+
const body = el('div', 'twm-diff__body');
|
|
88
|
+
root.append(switcher, body);
|
|
89
|
+
host.appendChild(root);
|
|
90
|
+
|
|
91
|
+
let data = {};
|
|
92
|
+
let chosen = options.view ?? null;
|
|
93
|
+
let textLines; // this data's Text view; undefined until first drawn
|
|
94
|
+
let current = null;
|
|
95
|
+
|
|
96
|
+
const hasPair = () => typeof data.before === 'string' || typeof data.after === 'string';
|
|
97
|
+
|
|
98
|
+
const linesOfText = () => {
|
|
99
|
+
if (textLines !== undefined) return textLines;
|
|
100
|
+
textLines = hasPair()
|
|
101
|
+
? computeLineDiff(data.before, data.after, {
|
|
102
|
+
context,
|
|
103
|
+
...(maxLines === undefined ? {} : { maxLines }),
|
|
104
|
+
beforeStart: data.beforeStart ?? 1,
|
|
105
|
+
afterStart: data.afterStart ?? 1,
|
|
106
|
+
})
|
|
107
|
+
: parseUnifiedDiff(data.diff);
|
|
108
|
+
return textLines;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
const offered = () => {
|
|
112
|
+
const out = [];
|
|
113
|
+
if (Array.isArray(data.fields)) out.push('fields');
|
|
114
|
+
if (hasPair() || (typeof data.diff === 'string' && linesOfText().length > 0)) out.push('text');
|
|
115
|
+
if (typeof data.diff === 'string') out.push('raw');
|
|
116
|
+
return out;
|
|
117
|
+
};
|
|
118
|
+
|
|
119
|
+
const pick = (views) => {
|
|
120
|
+
if (chosen && views.includes(chosen)) return chosen;
|
|
121
|
+
if (views.includes('fields') && data.fields.length > 0) return 'fields';
|
|
122
|
+
if (views.includes('text')) return 'text';
|
|
123
|
+
return views[0] ?? null;
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const message = (text) => {
|
|
127
|
+
body.replaceChildren(el('div', 'twm-diff__message', text));
|
|
128
|
+
};
|
|
129
|
+
|
|
130
|
+
const drawFields = () => {
|
|
131
|
+
if (data.fields.length === 0) { message(labels.noFields); return; }
|
|
132
|
+
const grid = el('div', 'twm-diff__fields');
|
|
133
|
+
for (const field of data.fields) {
|
|
134
|
+
const values = el('span', 'twm-diff__field-values');
|
|
135
|
+
const arrow = el('span', 'material-symbols-outlined twm-diff__field-arrow', 'arrow_forward');
|
|
136
|
+
arrow.setAttribute('aria-hidden', 'true');
|
|
137
|
+
values.append(
|
|
138
|
+
el('span', 'twm-diff__field-from', shownField(field.from)),
|
|
139
|
+
arrow,
|
|
140
|
+
el('span', 'twm-diff__field-to', shownField(field.to)),
|
|
141
|
+
);
|
|
142
|
+
grid.append(el('span', 'twm-diff__field-name', String(field.name)), values);
|
|
143
|
+
}
|
|
144
|
+
body.replaceChildren(grid);
|
|
145
|
+
};
|
|
146
|
+
|
|
147
|
+
const drawText = () => {
|
|
148
|
+
const lines = linesOfText();
|
|
149
|
+
if (lines === null) {
|
|
150
|
+
message(typeof data.diff === 'string' ? `${labels.tooLong} See ${labels.raw}.` : labels.tooLong);
|
|
151
|
+
return;
|
|
152
|
+
}
|
|
153
|
+
if (lines.length === 0) { message(labels.noChanges); return; }
|
|
154
|
+
const list = el('div', 'twm-diff__lines');
|
|
155
|
+
list.append(...lines.map((line) => lineRow(line, labels)));
|
|
156
|
+
body.replaceChildren(list);
|
|
157
|
+
};
|
|
158
|
+
|
|
159
|
+
const drawRaw = () => {
|
|
160
|
+
if (data.diff === '') { message(labels.noChanges); return; }
|
|
161
|
+
body.replaceChildren(el('pre', 'twm-diff__raw', data.diff));
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
const draw = () => {
|
|
165
|
+
const views = offered();
|
|
166
|
+
current = pick(views);
|
|
167
|
+
switcher.replaceChildren(...(views.length > 1 ? views.map((v) => {
|
|
168
|
+
const b = el('button', 'twm-diff__view', labels[v]);
|
|
169
|
+
b.type = 'button';
|
|
170
|
+
b.setAttribute('aria-pressed', String(v === current));
|
|
171
|
+
b.addEventListener('click', () => {
|
|
172
|
+
chosen = v;
|
|
173
|
+
draw();
|
|
174
|
+
onViewChange?.(v);
|
|
175
|
+
});
|
|
176
|
+
return b;
|
|
177
|
+
}) : []));
|
|
178
|
+
switcher.hidden = views.length <= 1;
|
|
179
|
+
if (current === 'fields') drawFields();
|
|
180
|
+
else if (current === 'text') drawText();
|
|
181
|
+
else if (current === 'raw') drawRaw();
|
|
182
|
+
else message(labels.noChanges);
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
const update = (next) => {
|
|
186
|
+
data = next ?? {};
|
|
187
|
+
textLines = undefined;
|
|
188
|
+
draw();
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
update(options);
|
|
192
|
+
|
|
193
|
+
return {
|
|
194
|
+
el: root,
|
|
195
|
+
update,
|
|
196
|
+
setView(view) {
|
|
197
|
+
chosen = view;
|
|
198
|
+
draw();
|
|
199
|
+
},
|
|
200
|
+
view: () => current,
|
|
201
|
+
/** Words in place of the diff, while it loads or when it could not be read. The next update replaces them. */
|
|
202
|
+
showMessage(text) {
|
|
203
|
+
switcher.replaceChildren();
|
|
204
|
+
switcher.hidden = true;
|
|
205
|
+
current = null;
|
|
206
|
+
message(text);
|
|
207
|
+
},
|
|
208
|
+
destroy() {
|
|
209
|
+
root.remove();
|
|
210
|
+
},
|
|
211
|
+
};
|
|
212
|
+
}
|