@gjermundgaraba-designs/pixel-paper 0.22.2 → 0.23.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/DESIGN.md +13 -13
- package/dist/src/adapters/mermaid.d.ts.map +1 -1
- package/dist/src/adapters/mermaid.js +0 -24
- package/dist/src/announcer/announcer.d.ts.map +1 -1
- package/dist/src/announcer/announcer.js +12 -12
- package/dist/src/board/board.d.ts +0 -10
- package/dist/src/board/board.d.ts.map +1 -1
- package/dist/src/board/board.js +36 -64
- package/dist/src/board/index.d.ts +2 -1
- package/dist/src/board/index.d.ts.map +1 -1
- package/dist/src/board/index.js +2 -1
- package/dist/src/button/button.d.ts +1 -1
- package/dist/src/button/button.d.ts.map +1 -1
- package/dist/src/commandPalette/commandPalette.d.ts.map +1 -1
- package/dist/src/commandPalette/commandPalette.js +78 -83
- package/dist/src/commandPalette/keys.d.ts +2 -2
- package/dist/src/commandPalette/keys.d.ts.map +1 -1
- package/dist/src/commandPalette/keys.js +2 -4
- package/dist/src/commandPalette/match.d.ts.map +1 -1
- package/dist/src/commandPalette/match.js +2 -2
- package/dist/src/confirmDialog/confirmDialog.d.ts.map +1 -1
- package/dist/src/confirmDialog/confirmDialog.js +1 -2
- package/dist/src/copyButton/copyButton.d.ts +0 -5
- package/dist/src/copyButton/copyButton.d.ts.map +1 -1
- package/dist/src/copyButton/copyButton.js +2 -2
- package/dist/src/crumbs/crumbs.d.ts +0 -2
- package/dist/src/crumbs/crumbs.d.ts.map +1 -1
- package/dist/src/crumbs/crumbs.js +2 -2
- package/dist/src/dialog/dialog.d.ts +2 -0
- package/dist/src/dialog/dialog.d.ts.map +1 -1
- package/dist/src/dialog/dialog.js +3 -2
- package/dist/src/drag/drag.d.ts +28 -0
- package/dist/src/drag/drag.d.ts.map +1 -1
- package/dist/src/drag/drag.js +39 -17
- package/dist/src/editableTitle/editableTitle.d.ts +4 -4
- package/dist/src/field/field.d.ts +7 -1
- package/dist/src/field/field.d.ts.map +1 -1
- package/dist/src/field/field.js +14 -6
- package/dist/src/fieldset/fieldset.d.ts.map +1 -1
- package/dist/src/fieldset/fieldset.js +2 -4
- package/dist/src/fileTree/fileTree.d.ts.map +1 -1
- package/dist/src/fileTree/fileTree.js +16 -16
- package/dist/src/inlineInput/inlineInput.d.ts +4 -0
- package/dist/src/inlineInput/inlineInput.d.ts.map +1 -1
- package/dist/src/inlineInput/inlineInput.js +9 -6
- package/dist/src/keyHelp/index.d.ts +1 -1
- package/dist/src/keyHelp/index.d.ts.map +1 -1
- package/dist/src/keyHelp/index.js +1 -1
- package/dist/src/keyHelp/keyHelp.d.ts +1 -3
- package/dist/src/keyHelp/keyHelp.d.ts.map +1 -1
- package/dist/src/keyHelp/keyHelp.js +3 -6
- package/dist/src/keyHint/keyLabel.d.ts +2 -0
- package/dist/src/keyHint/keyLabel.d.ts.map +1 -1
- package/dist/src/keyHint/keyLabel.js +3 -2
- package/dist/src/lineDiff/diff.d.ts +6 -3
- package/dist/src/lineDiff/diff.d.ts.map +1 -1
- package/dist/src/lineDiff/diff.js +40 -28
- package/dist/src/lineDiff/patch.d.ts.map +1 -1
- package/dist/src/lineDiff/patch.js +18 -13
- package/dist/src/linkState/linkState.d.ts.map +1 -1
- package/dist/src/linkState/linkState.js +1 -7
- package/dist/src/menu/slip.js +2 -2
- package/dist/src/meter/meter.d.ts.map +1 -1
- package/dist/src/meter/meter.js +22 -17
- package/dist/src/notice/notice.d.ts +2 -0
- package/dist/src/notice/notice.d.ts.map +1 -1
- package/dist/src/notice/notice.js +2 -1
- package/dist/src/passwordField/passwordField.d.ts.map +1 -1
- package/dist/src/passwordField/passwordField.js +3 -7
- package/dist/src/rail/rail.d.ts +17 -3
- package/dist/src/rail/rail.d.ts.map +1 -1
- package/dist/src/rail/rail.js +6 -6
- package/dist/src/reel/layout.d.ts +9 -5
- package/dist/src/reel/layout.d.ts.map +1 -1
- package/dist/src/reel/layout.js +11 -5
- package/dist/src/reel/reel.d.ts.map +1 -1
- package/dist/src/reel/reel.js +122 -68
- package/dist/src/scheme/scheme.d.ts +1 -1
- package/dist/src/scheme/scheme.d.ts.map +1 -1
- package/dist/src/segmented/segmented.d.ts +0 -3
- package/dist/src/segmented/segmented.d.ts.map +1 -1
- package/dist/src/segmented/segmented.js +1 -6
- package/dist/src/selectField/selectField.d.ts.map +1 -1
- package/dist/src/selectField/selectField.js +3 -10
- package/dist/src/sortable/index.d.ts +1 -1
- package/dist/src/sortable/index.d.ts.map +1 -1
- package/dist/src/sortable/index.js +1 -1
- package/dist/src/sortable/sortable.d.ts +39 -40
- package/dist/src/sortable/sortable.d.ts.map +1 -1
- package/dist/src/sortable/sortable.js +14 -31
- package/dist/src/stack/stack.d.ts +1 -1
- package/dist/src/stack/stack.d.ts.map +1 -1
- package/dist/src/stack/stack.js +1 -1
- package/dist/src/tabs/tabs.d.ts +1 -1
- package/dist/src/tabs/tabs.d.ts.map +1 -1
- package/dist/src/tabs/tabs.js +1 -1
- package/dist/src/textareaField/textareaField.d.ts.map +1 -1
- package/dist/src/textareaField/textareaField.js +3 -7
- package/dist/src/time/time.d.ts.map +1 -1
- package/dist/src/time/time.js +4 -2
- package/dist/src/tips/index.d.ts +1 -1
- package/dist/src/tips/index.d.ts.map +1 -1
- package/dist/src/toast/index.d.ts +1 -1
- package/dist/src/toast/index.d.ts.map +1 -1
- package/dist/src/toast/toast.d.ts +5 -5
- package/dist/src/toast/toast.d.ts.map +1 -1
- package/dist/src/toast/toast.js +1 -6
- package/dist/src/toolbar/toolbar.d.ts +0 -3
- package/dist/src/toolbar/toolbar.d.ts.map +1 -1
- package/dist/src/toolbar/toolbar.js +1 -2
- package/package.json +1 -1
- package/src/adapters/mermaid.ts +0 -24
- package/src/announcer/announcer.ts +13 -17
- package/src/board/board.ts +32 -80
- package/src/board/index.ts +1 -1
- package/src/button/button.ts +1 -1
- package/src/commandPalette/commandPalette.ts +21 -38
- package/src/commandPalette/keys.ts +2 -4
- package/src/commandPalette/match.ts +3 -3
- package/src/confirmDialog/confirmDialog.ts +5 -3
- package/src/copyButton/copyButton.ts +2 -5
- package/src/crumbs/crumbs.ts +2 -4
- package/src/dialog/dialog.ts +3 -2
- package/src/drag/drag.ts +54 -5
- package/src/field/field.ts +29 -7
- package/src/fieldset/fieldset.ts +2 -4
- package/src/fileTree/fileTree.ts +10 -12
- package/src/inlineInput/inlineInput.ts +11 -2
- package/src/keyHelp/index.ts +0 -1
- package/src/keyHelp/keyHelp.ts +3 -9
- package/src/keyHint/keyLabel.ts +5 -2
- package/src/lineDiff/diff.ts +70 -54
- package/src/lineDiff/patch.ts +27 -14
- package/src/linkState/linkState.ts +1 -8
- package/src/menu/slip.ts +2 -2
- package/src/meter/meter.ts +25 -15
- package/src/notice/notice.ts +2 -1
- package/src/passwordField/passwordField.ts +3 -7
- package/src/pixel-paper.css +82 -131
- package/src/rail/rail.ts +8 -6
- package/src/reel/layout.ts +14 -7
- package/src/reel/reel.ts +132 -63
- package/src/scheme/scheme.ts +1 -1
- package/src/segmented/segmented.ts +1 -9
- package/src/selectField/selectField.ts +3 -10
- package/src/sortable/index.ts +0 -1
- package/src/sortable/sortable.ts +35 -54
- package/src/stack/stack.ts +1 -1
- package/src/tabs/tabs.ts +1 -1
- package/src/textareaField/textareaField.ts +3 -7
- package/src/time/time.ts +6 -7
- package/src/tips/index.ts +1 -1
- package/src/toast/index.ts +1 -1
- package/src/toast/toast.ts +2 -8
- package/src/tokens.css +6 -0
- package/src/toolbar/toolbar.ts +0 -5
- package/dist/src/inlineInput/inline.d.ts +0 -6
- package/dist/src/inlineInput/inline.d.ts.map +0 -1
- package/dist/src/inlineInput/inline.js +0 -7
- package/src/inlineInput/inline.ts +0 -13
package/src/dialog/dialog.ts
CHANGED
|
@@ -89,7 +89,8 @@ export type UpdateContext = Readonly<{
|
|
|
89
89
|
isCloseHeld: boolean;
|
|
90
90
|
}>;
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
/** The context of a dialog whose close is not held, the default. */
|
|
93
|
+
export const FREE: UpdateContext = { isCloseHeld: false };
|
|
93
94
|
|
|
94
95
|
// NOTE: @foldkit/ui gap: Dialog has no way to hold a close. Escape, a press on the veil and a
|
|
95
96
|
// control given the `closeButton` group all send `RequestedClose`, so a held dialog reads it
|
|
@@ -248,7 +249,7 @@ export const view = defineView<Model, Message, ViewInputs>((model, viewInputs, h
|
|
|
248
249
|
} = viewInputs;
|
|
249
250
|
|
|
250
251
|
const actionBar = childAttributes([
|
|
251
|
-
h.
|
|
252
|
+
h.Attribute(ACTION_BAR_ATTRIBUTE, ""),
|
|
252
253
|
h.OnMount(BindActionKeys()),
|
|
253
254
|
]);
|
|
254
255
|
|
package/src/drag/drag.ts
CHANGED
|
@@ -1,13 +1,16 @@
|
|
|
1
1
|
// Shared by Board and Sortable, the two users of @foldkit/ui's DragAndDrop, and Reel, which
|
|
2
|
-
// holds its own: the phase a drag is in,
|
|
3
|
-
// lifts, Escape taken before anything else, the
|
|
4
|
-
// measure, and the closest-centre drop target a
|
|
5
|
-
// handlers are the Reel's and the Rail's.
|
|
2
|
+
// holds its own: the phase a drag is in, what a hold keeps and how a step settles it, the
|
|
3
|
+
// DragAndDrop Messages both build, what a press lifts, Escape taken before anything else, the
|
|
4
|
+
// pressing pointer's events, the lifted copy's measure, and the closest-centre drop target a
|
|
5
|
+
// sortable resolves for a pointer. A splitter's handlers are the Reel's and the Rail's.
|
|
6
6
|
|
|
7
7
|
import { DragAndDrop as UiDragAndDrop } from "@foldkit/ui";
|
|
8
|
-
import { Array, Effect, Option, Order, Stream, pipe } from "effect";
|
|
8
|
+
import { Array, Effect, Option, Order, Schema, Stream, pipe } from "effect";
|
|
9
|
+
import type * as Command from "foldkit/command";
|
|
9
10
|
import * as Dom from "foldkit/dom";
|
|
10
11
|
import type { Attribute, HtmlBuilder } from "foldkit/html";
|
|
12
|
+
import * as Subscription from "foldkit/subscription";
|
|
13
|
+
import * as Update from "foldkit/update";
|
|
11
14
|
|
|
12
15
|
import { isPlain } from "../keys/keys.ts";
|
|
13
16
|
|
|
@@ -36,6 +39,38 @@ export const phaseOf = (dragAndDrop: UiDragAndDrop.Model): Phase => {
|
|
|
36
39
|
export const isCarrying = (phase: Phase): boolean =>
|
|
37
40
|
phase === "PointerDragging" || phase === "KeyboardDragging";
|
|
38
41
|
|
|
42
|
+
// HOLD
|
|
43
|
+
|
|
44
|
+
/** What a Board's and a Sortable's Model share: the DragAndDrop, whether items may move, and
|
|
45
|
+
* what the list keeps while an item is pressed or held. */
|
|
46
|
+
type Holder = Readonly<{
|
|
47
|
+
dragAndDrop: UiDragAndDrop.Model;
|
|
48
|
+
isMovable: boolean;
|
|
49
|
+
maybeHeld: Option.Option<unknown>;
|
|
50
|
+
}>;
|
|
51
|
+
|
|
52
|
+
/** Whether an item is pressed or held. While it is, the arrow keys, Space and Escape belong to
|
|
53
|
+
* the list: pause the page's own bindings for them. The list takes Escape before anything
|
|
54
|
+
* else sees it. */
|
|
55
|
+
export const isHolding = (model: Holder): boolean => phaseOf(model.dragAndDrop) !== "Idle";
|
|
56
|
+
|
|
57
|
+
/** Whether a press may lift an item now: items may move and none is pressed or held. */
|
|
58
|
+
export const canLift = (model: Holder): boolean => model.isMovable && !isHolding(model);
|
|
59
|
+
|
|
60
|
+
/** Settles a DragAndDrop step: forgets the held item once it is down, and runs what the step
|
|
61
|
+
* says (`maybeAnnouncement`) after the step's own Commands. */
|
|
62
|
+
export const settle = <Model extends Holder, Message, OutMessage>(
|
|
63
|
+
step: Update.ReturnWithOutMessage<Model, Message, OutMessage>,
|
|
64
|
+
maybeAnnouncement: Option.Option<Command.Command<Message>>,
|
|
65
|
+
): Update.ReturnWithOutMessage<Model, Message, OutMessage> =>
|
|
66
|
+
Update.withOutMessage(
|
|
67
|
+
{
|
|
68
|
+
model: isHolding(step.model) ? step.model : { ...step.model, maybeHeld: Option.none() },
|
|
69
|
+
commands: [...(step.commands ?? []), ...Array.fromOption(maybeAnnouncement)],
|
|
70
|
+
},
|
|
71
|
+
step.outMessage,
|
|
72
|
+
);
|
|
73
|
+
|
|
39
74
|
// CHILD MESSAGES
|
|
40
75
|
|
|
41
76
|
// NOTE: @foldkit/ui gap: DragAndDrop exposes no update capability for a press, a keyboard
|
|
@@ -114,6 +149,20 @@ export const escapeKeys = <Message>(message: Message): Stream.Stream<Message> =>
|
|
|
114
149
|
options: { capture: true },
|
|
115
150
|
});
|
|
116
151
|
|
|
152
|
+
/** A list's `escape` Subscription: `escapeKeys` as `message` while an item is pressed or
|
|
153
|
+
* held. */
|
|
154
|
+
export const escapeSubscriptions = <Model extends Holder, Message>(message: Message) =>
|
|
155
|
+
Subscription.make<Model, Message>()((entry) => ({
|
|
156
|
+
escape: entry(
|
|
157
|
+
{ isHolding: Schema.Boolean },
|
|
158
|
+
{
|
|
159
|
+
modelToDependencies: (model) => ({ isHolding: isHolding(model) }),
|
|
160
|
+
dependenciesToStream: ({ isHolding: isHoldingNow }) =>
|
|
161
|
+
isHoldingNow ? escapeKeys(message) : Stream.empty,
|
|
162
|
+
},
|
|
163
|
+
),
|
|
164
|
+
}));
|
|
165
|
+
|
|
117
166
|
// POINTER
|
|
118
167
|
|
|
119
168
|
/** The moves, release and cancel of the pointer `pointerId`, on the document; another
|
package/src/field/field.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Input as UiInput } from "@foldkit/ui";
|
|
2
|
-
import type { Html, HtmlBuilder, TextareaAttribute } from "foldkit/html";
|
|
2
|
+
import type { Attribute, Html, HtmlBuilder, TextareaAttribute } from "foldkit/html";
|
|
3
3
|
|
|
4
4
|
import type { Attributes } from "../attributes/attributes.ts";
|
|
5
5
|
|
|
@@ -28,6 +28,32 @@ export const describedByError = <Message>(
|
|
|
28
28
|
? []
|
|
29
29
|
: [h.AriaDescribedBy(hasHelp ? `${errorId(id)} ${UiInput.descriptionId(id)}` : errorId(id))];
|
|
30
30
|
|
|
31
|
+
/** A field's control, after the `@foldkit/ui` attributes: described by its error before its
|
|
32
|
+
* help line, and `aria-required` when the form is not sent without it. */
|
|
33
|
+
// NOTE: @foldkit/ui gap: Input, Select and Textarea have no required option, so the wrapper
|
|
34
|
+
// says it.
|
|
35
|
+
export const controlAttributes = <Message>(
|
|
36
|
+
id: string,
|
|
37
|
+
error: string | undefined,
|
|
38
|
+
help: string | undefined,
|
|
39
|
+
isRequired: boolean,
|
|
40
|
+
h: HtmlBuilder<Message>,
|
|
41
|
+
): ReadonlyArray<TextareaAttribute<Message>> => [
|
|
42
|
+
...describedByError(id, error, help !== undefined, h),
|
|
43
|
+
...(isRequired ? [h.AriaRequired(true)] : []),
|
|
44
|
+
];
|
|
45
|
+
|
|
46
|
+
// HELP
|
|
47
|
+
|
|
48
|
+
/** The sans sentence under a field's controls, given one, with the `@foldkit/ui`
|
|
49
|
+
* `description` group that makes it describe them. */
|
|
50
|
+
export const helpView = <Message>(
|
|
51
|
+
help: string | undefined,
|
|
52
|
+
description: ReadonlyArray<Attribute<Message>>,
|
|
53
|
+
h: HtmlBuilder<Message>,
|
|
54
|
+
): ReadonlyArray<Html> =>
|
|
55
|
+
help === undefined ? [] : [h.p([...description, h.Class("field-help")], [help])];
|
|
56
|
+
|
|
31
57
|
// VIEW
|
|
32
58
|
|
|
33
59
|
/** Configuration for rendering a text field with `view`. */
|
|
@@ -92,17 +118,13 @@ export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Messag
|
|
|
92
118
|
[h.Class(`field${isBoxed || type === "date" ? " boxed" : ""}${isMono ? " mono" : ""}`)],
|
|
93
119
|
[
|
|
94
120
|
h.label([...groups.label, ...(isLabelHidden ? [h.Class("sr-only")] : [])], [label]),
|
|
95
|
-
// NOTE: @foldkit/ui gap: Input has no required option, so the wrapper says it.
|
|
96
121
|
h.input([
|
|
97
122
|
...inputAttributes,
|
|
98
123
|
...groups.input,
|
|
99
|
-
...
|
|
100
|
-
...(isRequired ? [h.AriaRequired(true)] : []),
|
|
124
|
+
...controlAttributes(id, error, help, isRequired, h),
|
|
101
125
|
]),
|
|
102
126
|
...errorView(id, error, h),
|
|
103
|
-
...(help
|
|
104
|
-
? []
|
|
105
|
-
: [h.p([...groups.description, h.Class("field-help")], [help])]),
|
|
127
|
+
...helpView(help, groups.description, h),
|
|
106
128
|
],
|
|
107
129
|
),
|
|
108
130
|
},
|
package/src/fieldset/fieldset.ts
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Fieldset as UiFieldset } from "@foldkit/ui";
|
|
2
2
|
import type { Html, HtmlBuilder } from "foldkit/html";
|
|
3
3
|
|
|
4
|
-
import { describedByError, errorView } from "../field/field.ts";
|
|
4
|
+
import { describedByError, errorView, helpView } from "../field/field.ts";
|
|
5
5
|
|
|
6
6
|
// VIEW
|
|
7
7
|
|
|
@@ -43,9 +43,7 @@ export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html
|
|
|
43
43
|
h.legend([...groups.legend, h.Class("field-label")], [label]),
|
|
44
44
|
...body,
|
|
45
45
|
...errorView(id, error, h),
|
|
46
|
-
...(help
|
|
47
|
-
? []
|
|
48
|
-
: [h.p([...groups.description, h.Class("field-help")], [help])]),
|
|
46
|
+
...helpView(help, groups.description, h),
|
|
49
47
|
],
|
|
50
48
|
),
|
|
51
49
|
},
|
package/src/fileTree/fileTree.ts
CHANGED
|
@@ -209,18 +209,16 @@ const moveInTree = (tree: Element, event: KeyboardEvent): Option.Option<TreeKeyA
|
|
|
209
209
|
const items = ownItems(tree);
|
|
210
210
|
|
|
211
211
|
return pipe(
|
|
212
|
-
Array.
|
|
213
|
-
Option.flatMap((index) =>
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
}),
|
|
223
|
-
),
|
|
212
|
+
Array.findFirstWithIndex(items, (item) => item === event.target),
|
|
213
|
+
Option.flatMap(([item, index]) => {
|
|
214
|
+
if (isTypeahead(event)) {
|
|
215
|
+
return moveTo(itemStartingWith(items, index, event.key));
|
|
216
|
+
} else if (isPlain(event)) {
|
|
217
|
+
return actOnKey(items, index, item, event.key);
|
|
218
|
+
} else {
|
|
219
|
+
return Option.none();
|
|
220
|
+
}
|
|
221
|
+
}),
|
|
224
222
|
);
|
|
225
223
|
};
|
|
226
224
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { Input as UiInput } from "@foldkit/ui";
|
|
2
|
-
import { Effect, Match, Option, Schema } from "effect";
|
|
2
|
+
import { Effect, Match, Option, Schema, String, pipe } from "effect";
|
|
3
3
|
import * as Command from "foldkit/command";
|
|
4
4
|
import * as Dom from "foldkit/dom";
|
|
5
5
|
import { type Html, type HtmlBuilder } from "foldkit/html";
|
|
@@ -12,7 +12,6 @@ import * as Update from "foldkit/update";
|
|
|
12
12
|
|
|
13
13
|
import { isPlain } from "../keys/keys.ts";
|
|
14
14
|
import * as Notice from "../notice/notice.ts";
|
|
15
|
-
import { toWrite } from "./inline.ts";
|
|
16
15
|
|
|
17
16
|
// MODEL
|
|
18
17
|
|
|
@@ -141,6 +140,16 @@ const end = (model: Model, how: InlineEnd): UpdateReturn => ({
|
|
|
141
140
|
outMessage: OutMessage.Ended({ how, maybeLastSavedId: model.maybeLastSavedId }),
|
|
142
141
|
});
|
|
143
142
|
|
|
143
|
+
/** What Enter or leaving an inline field writes: the draft trimmed, or none (an untouched
|
|
144
|
+
* draft, so an edit made elsewhere to `value` is never overwritten; an empty one; or the
|
|
145
|
+
* text it replaces). */
|
|
146
|
+
export const toWrite = (maybeDraft: Option.Option<string>, value: string): Option.Option<string> =>
|
|
147
|
+
pipe(
|
|
148
|
+
maybeDraft,
|
|
149
|
+
Option.map(String.trim),
|
|
150
|
+
Option.filter((text) => String.isNonEmpty(text) && text !== value),
|
|
151
|
+
);
|
|
152
|
+
|
|
144
153
|
const commit = (model: Model, trigger: SaveTrigger): UpdateReturn =>
|
|
145
154
|
Option.match(toWrite(model.maybeDraft, model.replaced), {
|
|
146
155
|
onNone: () => end(model, trigger),
|
package/src/keyHelp/index.ts
CHANGED
package/src/keyHelp/keyHelp.ts
CHANGED
|
@@ -13,7 +13,7 @@ import * as Card from "../card/card.ts";
|
|
|
13
13
|
import * as Dialog from "../dialog/dialog.ts";
|
|
14
14
|
import { foldDialog } from "../dialog/fold.ts";
|
|
15
15
|
import * as Field from "../field/field.ts";
|
|
16
|
-
import { type KeyPlatform, keyLabel } from "../keyHint/keyLabel.ts";
|
|
16
|
+
import { type KeyPlatform, keyLabel, pressesOf } from "../keyHint/keyLabel.ts";
|
|
17
17
|
import * as Keymap from "../keymap/keymap.ts";
|
|
18
18
|
|
|
19
19
|
// MODEL
|
|
@@ -54,7 +54,7 @@ export type Message = typeof Message.Type;
|
|
|
54
54
|
// OUT MESSAGE
|
|
55
55
|
|
|
56
56
|
/** Union of OutMessages the key help reports: the reader closed it (Escape, a click on the
|
|
57
|
-
* veil, a close button). Opening it, by `?` or `open`,
|
|
57
|
+
* veil, a close button). Opening it, by `?` or `open`, reports nothing. */
|
|
58
58
|
export const OutMessage = defineMessageUnion({
|
|
59
59
|
Closed: {},
|
|
60
60
|
});
|
|
@@ -99,9 +99,6 @@ export const update = (model: Model, message: Message) =>
|
|
|
99
99
|
export const open = (model: Model): Update.Return<Model, Message> =>
|
|
100
100
|
dialogFold.open(clearQuery(model));
|
|
101
101
|
|
|
102
|
-
/** Closes the key help without reporting `Closed`. */
|
|
103
|
-
export const close = (model: Model): Update.Return<Model, Message> => dialogFold.close(model);
|
|
104
|
-
|
|
105
102
|
// ACCESSORS
|
|
106
103
|
|
|
107
104
|
/** The key help's id, unique in the document. */
|
|
@@ -134,12 +131,9 @@ export const subscriptions = Subscription.make<Model, Message>()((entry) => ({
|
|
|
134
131
|
|
|
135
132
|
// LISTING
|
|
136
133
|
|
|
137
|
-
const keysOf = (keys: KeySequence): ReadonlyArray<string> =>
|
|
138
|
-
Predicate.isString(keys) ? [keys] : keys;
|
|
139
|
-
|
|
140
134
|
const isFound = (binding: Binding, labels: string, query: string): boolean =>
|
|
141
135
|
pipe(
|
|
142
|
-
[labels, ...Array.flatMap(binding.keys,
|
|
136
|
+
[labels, ...Array.flatMap(binding.keys, pressesOf), binding.does, binding.group],
|
|
143
137
|
Array.join(" "),
|
|
144
138
|
String.toLowerCase,
|
|
145
139
|
String.includes(query),
|
package/src/keyHint/keyLabel.ts
CHANGED
|
@@ -110,6 +110,10 @@ const pressLabel = (press: string, platform: KeyPlatform): string => {
|
|
|
110
110
|
return `${prefix}${keyName(key, Array.isReadonlyArrayNonEmpty(orderedModifiers))}`;
|
|
111
111
|
};
|
|
112
112
|
|
|
113
|
+
/** The presses a binding's keys name: one, or a sequence. */
|
|
114
|
+
export const pressesOf = (keys: KeySequence): ReadonlyArray<string> =>
|
|
115
|
+
Predicate.isString(keys) ? [keys] : keys;
|
|
116
|
+
|
|
113
117
|
/**
|
|
114
118
|
* A key binding as the help and the key hints print it. Takes the same strings
|
|
115
119
|
* `Dom.streamFromKeyBindings` matches: `"Mod+K"` prints `⌘K` on Apple and `Ctrl+K` elsewhere,
|
|
@@ -119,8 +123,7 @@ const pressLabel = (press: string, platform: KeyPlatform): string => {
|
|
|
119
123
|
* named key keeps its spaces.
|
|
120
124
|
*/
|
|
121
125
|
export const keyLabel = (keys: KeySequence, platform: KeyPlatform): string => {
|
|
122
|
-
const
|
|
123
|
-
const labels = Array.map(presses, (press) => pressLabel(press, platform));
|
|
126
|
+
const labels = Array.map(pressesOf(keys), (press) => pressLabel(press, platform));
|
|
124
127
|
|
|
125
128
|
return Array.every(labels, isCharacter) ? Array.join(labels, "") : Array.join(labels, " ");
|
|
126
129
|
};
|
package/src/lineDiff/diff.ts
CHANGED
|
@@ -49,6 +49,14 @@ const numberLine = (numbers: Numbers, kind: LineKind, text: string): [Numbers, D
|
|
|
49
49
|
Match.exhaustive,
|
|
50
50
|
);
|
|
51
51
|
|
|
52
|
+
type Line = Readonly<{ kind: LineKind; text: string }>;
|
|
53
|
+
|
|
54
|
+
const numbered = (
|
|
55
|
+
lines: ReadonlyArray<Line>,
|
|
56
|
+
from: Numbers,
|
|
57
|
+
): readonly [Numbers, ReadonlyArray<DiffRow>] =>
|
|
58
|
+
Array.mapAccum(lines, from, (numbers, { kind, text }) => numberLine(numbers, kind, text));
|
|
59
|
+
|
|
52
60
|
/** The line diff of two texts (Myers, through jsdiff), each line numbered on its side. A
|
|
53
61
|
* missing final newline is not a change. */
|
|
54
62
|
export const diffRowsOf = (before: string, after: string): ReadonlyArray<DiffRow> =>
|
|
@@ -60,21 +68,16 @@ export const diffRowsOf = (before: string, after: string): ReadonlyArray<DiffRow
|
|
|
60
68
|
text,
|
|
61
69
|
})),
|
|
62
70
|
),
|
|
63
|
-
(lines) =>
|
|
64
|
-
Array.mapAccum(lines, { old: 1, new: 1 }, (numbers, { kind, text }) =>
|
|
65
|
-
numberLine(numbers, kind, text),
|
|
66
|
-
),
|
|
67
|
-
([, rows]) => rows,
|
|
71
|
+
(lines) => numbered(lines, { old: 1, new: 1 })[1],
|
|
68
72
|
);
|
|
69
73
|
|
|
74
|
+
const isChange = (row: DiffRow): boolean => row._tag === "Added" || row._tag === "Removed";
|
|
75
|
+
|
|
70
76
|
/** Whether a line was added or removed. */
|
|
71
|
-
export const hasChanges = (rows: ReadonlyArray<DiffRow>): boolean =>
|
|
72
|
-
Array.some(rows, (row) => row._tag === "Added" || row._tag === "Removed");
|
|
77
|
+
export const hasChanges = (rows: ReadonlyArray<DiffRow>): boolean => Array.some(rows, isChange);
|
|
73
78
|
|
|
74
79
|
// PATCH
|
|
75
80
|
|
|
76
|
-
type PatchState = Readonly<{ rows: ReadonlyArray<DiffRow>; numbers: Numbers }>;
|
|
77
|
-
|
|
78
81
|
const LINE_KIND_BY_PREFIX: Readonly<Record<string, LineKind>> = {
|
|
79
82
|
" ": "Same",
|
|
80
83
|
"+": "Added",
|
|
@@ -86,23 +89,27 @@ const LINE_KIND_BY_PREFIX: Readonly<Record<string, LineKind>> = {
|
|
|
86
89
|
const kindOf = (line: string): Option.Option<LineKind> =>
|
|
87
90
|
line === "" ? Option.some("Same") : Option.fromUndefinedOr(LINE_KIND_BY_PREFIX[line.charAt(0)]);
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
{ rows: [...rows, ...gapRows], numbers: { old: hunk.oldStart, new: hunk.newStart } },
|
|
96
|
-
(state: PatchState, line): PatchState =>
|
|
97
|
-
// "" starts with a "\", a prefix that draws no row
|
|
98
|
-
Option.match(kindOf(line), {
|
|
99
|
-
onNone: () => state,
|
|
100
|
-
onSome: (kind) => {
|
|
101
|
-
const [nextNumbers, row] = numberLine(state.numbers, kind, line.slice(1));
|
|
102
|
-
return { rows: [...state.rows, row], numbers: nextNumbers };
|
|
103
|
-
},
|
|
104
|
-
}),
|
|
92
|
+
// "" starts with a "\", a prefix that draws no row
|
|
93
|
+
const linesOf = (hunk: StructuredPatchHunk): ReadonlyArray<Line> =>
|
|
94
|
+
Array.getSomes(
|
|
95
|
+
Array.map(hunk.lines, (line) =>
|
|
96
|
+
Option.map(kindOf(line), (kind) => ({ kind, text: line.slice(1) })),
|
|
97
|
+
),
|
|
105
98
|
);
|
|
99
|
+
|
|
100
|
+
// each hunk is numbered from its own header; only the gap before it needs the old line the
|
|
101
|
+
// previous one ended on
|
|
102
|
+
const readHunk = (
|
|
103
|
+
nextOld: number,
|
|
104
|
+
hunk: StructuredPatchHunk,
|
|
105
|
+
): readonly [number, ReadonlyArray<DiffRow>] => {
|
|
106
|
+
const gap = hunk.oldStart - nextOld;
|
|
107
|
+
const [end, rows] = numbered(linesOf(hunk), { old: hunk.oldStart, new: hunk.newStart });
|
|
108
|
+
|
|
109
|
+
return [
|
|
110
|
+
end.old,
|
|
111
|
+
gap > 0 ? [DiffRow.Fold({ count: gap, firstOldNumber: nextOld }), ...rows] : rows,
|
|
112
|
+
];
|
|
106
113
|
};
|
|
107
114
|
|
|
108
115
|
const onlyHunksOf = <File extends Readonly<{ hunks: Array<StructuredPatchHunk> }>>(
|
|
@@ -116,35 +123,29 @@ const onlyHunksOf = <File extends Readonly<{ hunks: Array<StructuredPatchHunk> }
|
|
|
116
123
|
: Option.none(),
|
|
117
124
|
});
|
|
118
125
|
|
|
119
|
-
/** The
|
|
120
|
-
* lines between hunks folded into a count. `None` unless it is one file with a hunk: text that
|
|
126
|
+
/** The hunks of one file's unified diff. `None` unless it is one file with a hunk: text that
|
|
121
127
|
* is not a diff, a binary change and several files have no lines to show. */
|
|
128
|
+
export const hunksOfPatch = (
|
|
129
|
+
patch: string,
|
|
130
|
+
): Option.Option<Array.NonEmptyReadonlyArray<StructuredPatchHunk>> =>
|
|
131
|
+
Option.flatMap(Option.liftThrowable(parsePatch)(patch), onlyHunksOf);
|
|
132
|
+
|
|
133
|
+
/** The rows of one file's unified diff, numbered from its hunks' headers, with the unchanged
|
|
134
|
+
* lines between hunks folded into a count. `None` where `hunksOfPatch` is. */
|
|
122
135
|
export const diffRowsOfPatch = (patch: string): Option.Option<ReadonlyArray<DiffRow>> =>
|
|
123
|
-
|
|
124
|
-
Option.liftThrowable(parsePatch)(patch.replaceAll("\r\n", "\n")),
|
|
125
|
-
Option.flatMap(onlyHunksOf),
|
|
126
|
-
Option.map(
|
|
127
|
-
(hunks) => Array.reduce(hunks, { rows: [], numbers: { old: 1, new: 1 } }, readHunk).rows,
|
|
128
|
-
),
|
|
129
|
-
);
|
|
136
|
+
Option.map(hunksOfPatch(patch), (hunks) => Array.flatten(Array.mapAccum(hunks, 1, readHunk)[1]));
|
|
130
137
|
|
|
131
138
|
// FOLD
|
|
132
139
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
folded: ReadonlyArray<SameRow>;
|
|
136
|
-
}>;
|
|
140
|
+
/** A run of unchanged lines being folded: its first line and how many there are. */
|
|
141
|
+
type Run = Readonly<{ first: SameRow; count: number }>;
|
|
137
142
|
|
|
138
143
|
// a run of one stays a line: a row that says "1 unchanged line" takes the same room as the line
|
|
139
|
-
const
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
:
|
|
143
|
-
|
|
144
|
-
const flushFold = ({ rows, folded }: FoldState): ReadonlyArray<DiffRow> =>
|
|
145
|
-
Array.match(folded, {
|
|
146
|
-
onEmpty: () => rows,
|
|
147
|
-
onNonEmpty: (run) => [...rows, foldRow(run)],
|
|
144
|
+
const runRows = (maybeRun: Option.Option<Run>): ReadonlyArray<DiffRow> =>
|
|
145
|
+
Option.match(maybeRun, {
|
|
146
|
+
onNone: () => [],
|
|
147
|
+
onSome: ({ first, count }) =>
|
|
148
|
+
count === 1 ? [first] : [DiffRow.Fold({ count, firstOldNumber: first.oldNumber })],
|
|
148
149
|
});
|
|
149
150
|
|
|
150
151
|
/** The diff's rows: every change with `context` unchanged lines around it, and each longer
|
|
@@ -153,14 +154,29 @@ export const foldUnchanged = (
|
|
|
153
154
|
rows: ReadonlyArray<DiffRow>,
|
|
154
155
|
context: number,
|
|
155
156
|
): ReadonlyArray<DiffRow> => {
|
|
157
|
+
// the changes before each index, so the changes in any window are one subtraction
|
|
158
|
+
const changesBefore = Array.scan(rows, 0, (count, row) => count + (isChange(row) ? 1 : 0));
|
|
156
159
|
const isKept = (index: number): boolean =>
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
160
|
+
Array.getUnsafe(changesBefore, Math.min(rows.length, index + context + 1)) -
|
|
161
|
+
Array.getUnsafe(changesBefore, Math.max(0, index - context)) >
|
|
162
|
+
0;
|
|
163
|
+
|
|
164
|
+
const [maybeRun, kept] = Array.mapAccum(
|
|
165
|
+
rows,
|
|
166
|
+
Option.none<Run>(),
|
|
167
|
+
(maybeRun, row, index): readonly [Option.Option<Run>, ReadonlyArray<DiffRow>] =>
|
|
161
168
|
row._tag === "Same" && !isKept(index)
|
|
162
|
-
?
|
|
163
|
-
|
|
164
|
-
|
|
169
|
+
? [
|
|
170
|
+
Option.some(
|
|
171
|
+
Option.match(maybeRun, {
|
|
172
|
+
onNone: () => ({ first: row, count: 1 }),
|
|
173
|
+
onSome: (run) => ({ ...run, count: run.count + 1 }),
|
|
174
|
+
}),
|
|
175
|
+
),
|
|
176
|
+
[],
|
|
177
|
+
]
|
|
178
|
+
: [Option.none(), [...runRows(maybeRun), row]],
|
|
165
179
|
);
|
|
180
|
+
|
|
181
|
+
return [...Array.flatten(kept), ...runRows(maybeRun)];
|
|
166
182
|
};
|
package/src/lineDiff/patch.ts
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import { Array, Option, String, pipe } from "effect";
|
|
2
2
|
|
|
3
|
-
import { type DiffRow, diffRowsOfPatch } from "./diff.ts";
|
|
3
|
+
import { type DiffRow, diffRowsOfPatch, hunksOfPatch } from "./diff.ts";
|
|
4
4
|
|
|
5
5
|
// QUOTED PATHS
|
|
6
6
|
|
|
@@ -114,9 +114,12 @@ const lineNote = (head: ReadonlyArray<string>, hasRows: boolean): Option.Option<
|
|
|
114
114
|
}
|
|
115
115
|
};
|
|
116
116
|
|
|
117
|
-
|
|
117
|
+
/** How a section's lines are read: `None` where it has none to show. */
|
|
118
|
+
type RowsOf = (section: string) => Option.Option<ReadonlyArray<DiffRow>>;
|
|
119
|
+
|
|
120
|
+
const readSection = (section: string, rowsOf: RowsOf): PatchReading => {
|
|
118
121
|
const head = headOf(section);
|
|
119
|
-
const maybeRows =
|
|
122
|
+
const maybeRows = rowsOf(section);
|
|
120
123
|
const maybeRenamedFrom = Option.map(valueAfter(head, "rename from "), unquoted);
|
|
121
124
|
const notes = [
|
|
122
125
|
...Option.toArray(Option.map(maybeRenamedFrom, (from) => `Renamed from ${from}.`)),
|
|
@@ -150,14 +153,16 @@ const KINDS: Readonly<Record<string, string>> = {
|
|
|
150
153
|
|
|
151
154
|
const kindOf = (mode: string): string => KINDS[mode] ?? `mode ${mode}`;
|
|
152
155
|
|
|
153
|
-
const sideTable = (section: string, side: string): ReadonlyArray<Table> =>
|
|
154
|
-
Option.toArray(
|
|
155
|
-
Option.map(diffRowsOfPatch(section), (rows) => ({ rows, maybeSide: Option.some(side) })),
|
|
156
|
-
);
|
|
156
|
+
const sideTable = (section: string, side: string, rowsOf: RowsOf): ReadonlyArray<Table> =>
|
|
157
|
+
Option.toArray(Option.map(rowsOf(section), (rows) => ({ rows, maybeSide: Option.some(side) })));
|
|
157
158
|
|
|
158
159
|
// a file that became a symlink (or any other kind) is two sections for one path: the old
|
|
159
160
|
// kind deleted, then the new one added
|
|
160
|
-
const readTypeChange = (
|
|
161
|
+
const readTypeChange = (
|
|
162
|
+
gone: string,
|
|
163
|
+
added: string,
|
|
164
|
+
rowsOf: RowsOf,
|
|
165
|
+
): Option.Option<PatchReading> => {
|
|
161
166
|
const [goneHead, addedHead] = [headOf(gone), headOf(added)];
|
|
162
167
|
|
|
163
168
|
return Option.flatMap(
|
|
@@ -170,14 +175,16 @@ const readTypeChange = (gone: string, added: string): Option.Option<PatchReading
|
|
|
170
175
|
? Option.some({
|
|
171
176
|
maybeRenamedFrom: Option.none(),
|
|
172
177
|
notes: [`It was ${kindOf(before)} and is now ${kindOf(after)}.`],
|
|
173
|
-
tables: [
|
|
178
|
+
tables: [
|
|
179
|
+
...sideTable(gone, "as it was", rowsOf),
|
|
180
|
+
...sideTable(added, "as it is", rowsOf),
|
|
181
|
+
],
|
|
174
182
|
})
|
|
175
183
|
: Option.none(),
|
|
176
184
|
);
|
|
177
185
|
};
|
|
178
186
|
|
|
179
|
-
|
|
180
|
-
export const readPatch = (patch: string): PatchReading => {
|
|
187
|
+
const readPatchWith = (patch: string, rowsOf: RowsOf): PatchReading => {
|
|
181
188
|
const text = patch.replaceAll("\r\n", "\n");
|
|
182
189
|
|
|
183
190
|
if (String.isEmpty(String.trim(text))) {
|
|
@@ -187,15 +194,18 @@ export const readPatch = (patch: string): PatchReading => {
|
|
|
187
194
|
const [first, second, ...rest] = sections;
|
|
188
195
|
|
|
189
196
|
if (first !== undefined && second === undefined) {
|
|
190
|
-
return readSection(first);
|
|
197
|
+
return readSection(first, rowsOf);
|
|
191
198
|
} else if (first !== undefined && second !== undefined && Array.isArrayEmpty(rest)) {
|
|
192
|
-
return Option.getOrElse(readTypeChange(first, second), () => unreadable);
|
|
199
|
+
return Option.getOrElse(readTypeChange(first, second, rowsOf), () => unreadable);
|
|
193
200
|
} else {
|
|
194
201
|
return unreadable;
|
|
195
202
|
}
|
|
196
203
|
}
|
|
197
204
|
};
|
|
198
205
|
|
|
206
|
+
/** What `patchView` makes of a patch: the notes it prints and the tables it draws. */
|
|
207
|
+
export const readPatch = (patch: string): PatchReading => readPatchWith(patch, diffRowsOfPatch);
|
|
208
|
+
|
|
199
209
|
// DESCRIPTION
|
|
200
210
|
|
|
201
211
|
/** What a patch says beyond its lines, as `patchView` prints it. */
|
|
@@ -212,7 +222,10 @@ export type PatchDescription = Readonly<{
|
|
|
212
222
|
* prints (`["Renamed from old.txt."]`), for a caller that prints them itself, and the path a
|
|
213
223
|
* rename left. */
|
|
214
224
|
export const describePatch = (patch: string): PatchDescription => {
|
|
215
|
-
|
|
225
|
+
// the notes need only whether a section has lines, so none are numbered
|
|
226
|
+
const { maybeRenamedFrom, notes } = readPatchWith(patch, (section) =>
|
|
227
|
+
Option.as(hunksOfPatch(section), []),
|
|
228
|
+
);
|
|
216
229
|
|
|
217
230
|
return { maybeRenamedFrom, notes };
|
|
218
231
|
};
|
|
@@ -27,13 +27,6 @@ export type ViewConfig = Readonly<{
|
|
|
27
27
|
label?: string;
|
|
28
28
|
}>;
|
|
29
29
|
|
|
30
|
-
const STATE_CLASS: Readonly<Record<State, string>> = {
|
|
31
|
-
Online: "",
|
|
32
|
-
Connecting: " connecting",
|
|
33
|
-
Reconnecting: " reconnecting",
|
|
34
|
-
Offline: " offline",
|
|
35
|
-
};
|
|
36
|
-
|
|
37
30
|
/** The connection word, in a top bar or a status bar. It is a status (`role="status"`), so a
|
|
38
31
|
* change of state is read out; while it connects it prints the working cursor. */
|
|
39
32
|
export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html => {
|
|
@@ -41,7 +34,7 @@ export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html
|
|
|
41
34
|
const isWorking = state === "Connecting" || state === "Reconnecting";
|
|
42
35
|
|
|
43
36
|
return h.span(
|
|
44
|
-
[h.Role("status"), h.Class(`link-state${
|
|
37
|
+
[h.Role("status"), h.Class(`link-state${state === "Online" ? "" : ` ${stateWord(state)}`}`)],
|
|
45
38
|
[label, ...(isWorking ? [h.span([h.Class("cursor"), h.AriaHidden(true)])] : [])],
|
|
46
39
|
);
|
|
47
40
|
};
|
package/src/menu/slip.ts
CHANGED
|
@@ -58,8 +58,8 @@ const edgeAttributes = <Message>(
|
|
|
58
58
|
id: string,
|
|
59
59
|
h: HtmlBuilder<Message>,
|
|
60
60
|
) => [
|
|
61
|
-
...(Option.contains(context.firstId, id) ? [h.
|
|
62
|
-
...(Option.contains(context.lastId, id) ? [h.
|
|
61
|
+
...(Option.contains(context.firstId, id) ? [h.Attribute(FIRST_ATTRIBUTE, "")] : []),
|
|
62
|
+
...(Option.contains(context.lastId, id) ? [h.Attribute(LAST_ATTRIBUTE, "")] : []),
|
|
63
63
|
];
|
|
64
64
|
|
|
65
65
|
const wordsView = <Message>(
|