@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/rail/rail.ts
CHANGED
|
@@ -87,7 +87,7 @@ export const init = (config: InitConfig): Model => ({
|
|
|
87
87
|
* (which leaves the deck at least 421px of a 901px window, the narrowest one the rail stands
|
|
88
88
|
* in, more than a phone has), and what an arrow on its splitter moves it. The bounds and the
|
|
89
89
|
* step are the component's, not tokens: the splitter says them as its values. */
|
|
90
|
-
const WIDTH = { start: 272, least: 220, most: 480, step: 16 };
|
|
90
|
+
export const WIDTH = { start: 272, least: 220, most: 480, step: 16 };
|
|
91
91
|
|
|
92
92
|
const bounded = (width: number): number =>
|
|
93
93
|
Math.round(Number.clamp(width, { minimum: WIDTH.least, maximum: WIDTH.most }));
|
|
@@ -238,7 +238,7 @@ type DrawerMessage =
|
|
|
238
238
|
| typeof Message.WidenedPastDrawer.Type;
|
|
239
239
|
|
|
240
240
|
/** The width at and under which the rail is a drawer, as the stylesheet has it. */
|
|
241
|
-
const DRAWER_QUERY = "(max-width: 900px)";
|
|
241
|
+
export const DRAWER_QUERY = "(max-width: 900px)";
|
|
242
242
|
|
|
243
243
|
/** The window crossing the drawer's width: growing past it while the drawer is open closes
|
|
244
244
|
* the drawer, since the rail is back in the page there. */
|
|
@@ -373,9 +373,9 @@ export type ViewInputs = Readonly<{
|
|
|
373
373
|
*
|
|
374
374
|
* Its splitter sizes it, by a pointer (previewed as it moves) or the side arrows, within its
|
|
375
375
|
* bounds, and reports the width as `ResizedRail` for the parent to keep and pass back as
|
|
376
|
-
* `maybeWidth`, which the view sets as `--
|
|
377
|
-
* rail's width follow it. The drawer keeps its own width. Lift
|
|
378
|
-
* the application's, or a dragged splitter follows nothing.
|
|
376
|
+
* `maybeWidth`, which the view sets as `--railed-width` on `div.railed`, so the page's padding
|
|
377
|
+
* and the rail's width follow it (without one, `--rail`). The drawer keeps its own width. Lift
|
|
378
|
+
* `subscriptions(keyPrefix)` into the application's, or a dragged splitter follows nothing.
|
|
379
379
|
*
|
|
380
380
|
* Its state is its own, not `@foldkit/ui`'s Popover: that panel is placed by Floating UI and
|
|
381
381
|
* rendered only while open, and this one is always in the page, placed by the stylesheet, and
|
|
@@ -414,7 +414,9 @@ export const view = defineView<Model, Message, ViewInputs>((model, viewInputs, h
|
|
|
414
414
|
return h.div(
|
|
415
415
|
[
|
|
416
416
|
h.Class("railed"),
|
|
417
|
-
...Array.fromOption(
|
|
417
|
+
...Array.fromOption(
|
|
418
|
+
Option.map(maybeWidth, (shown) => h.Style({ "--railed-width": `${shown}px` })),
|
|
419
|
+
),
|
|
418
420
|
],
|
|
419
421
|
[
|
|
420
422
|
toDeckView(openButton),
|
package/src/reel/layout.ts
CHANGED
|
@@ -247,6 +247,11 @@ export const apply = (layout: Layout, request: OutMessage): Layout =>
|
|
|
247
247
|
})),
|
|
248
248
|
});
|
|
249
249
|
|
|
250
|
+
/** Whether `request` changes `layout`: a move, a drop or a stop that would leave it as it is
|
|
251
|
+
* is passed over. */
|
|
252
|
+
export const isChange = (layout: Layout, request: OutMessage): boolean =>
|
|
253
|
+
!Equal.equals(apply(layout, request), layout);
|
|
254
|
+
|
|
250
255
|
// WIDTHS
|
|
251
256
|
|
|
252
257
|
/** How close a width is to a preset to count as it. */
|
|
@@ -349,7 +354,7 @@ export const requestOf = (
|
|
|
349
354
|
OutMessage.MovedPane({ paneId, toColumnId: columnId, toIndex: index }),
|
|
350
355
|
Gap: ({ maybeBeforeColumnId }) => OutMessage.PulledOutPane({ paneId, maybeBeforeColumnId }),
|
|
351
356
|
}),
|
|
352
|
-
(request) =>
|
|
357
|
+
(request) => isChange(layout, request),
|
|
353
358
|
);
|
|
354
359
|
|
|
355
360
|
/** Where the pane `paneId` would be once dropped at `target`, in words. */
|
|
@@ -469,13 +474,11 @@ export const steppedTarget = (
|
|
|
469
474
|
const EDGE_SHARE = 0.2;
|
|
470
475
|
const EDGE_MAX = 48;
|
|
471
476
|
|
|
472
|
-
/** A column as a pointer reads it: its id
|
|
473
|
-
* of its slots, the held pane's left out. */
|
|
477
|
+
/** A column as a pointer reads it: its id and its left and right edges. */
|
|
474
478
|
export type ColumnBox = Readonly<{
|
|
475
479
|
id: string;
|
|
476
480
|
left: number;
|
|
477
481
|
right: number;
|
|
478
|
-
slotMiddles: ReadonlyArray<number>;
|
|
479
482
|
}>;
|
|
480
483
|
|
|
481
484
|
const gapBefore = (columns: ReadonlyArray<ColumnBox>, gap: number): Target =>
|
|
@@ -484,18 +487,22 @@ const gapBefore = (columns: ReadonlyArray<ColumnBox>, gap: number): Target =>
|
|
|
484
487
|
/** Where a pointer at (`clientX`, `clientY`) over a reel whose columns sit at `columns`, left
|
|
485
488
|
* to right, would drop a pane. Over a column's edge band (or over a gap, or past the last
|
|
486
489
|
* column) it lands in that gap, as a column of its own; over the rest of a column it lands
|
|
487
|
-
* among that column's other panes, after each whose middle is above the pointer.
|
|
490
|
+
* among that column's other panes, after each whose middle is above the pointer.
|
|
491
|
+
* `slotMiddlesOf` gives the vertical middles of a column's slots, by its index, the held
|
|
492
|
+
* pane's left out: it is asked for the column the pointer is over alone, and only when the
|
|
493
|
+
* pointer is among its panes. */
|
|
488
494
|
export const targetAtPoint = (
|
|
489
495
|
columns: ReadonlyArray<ColumnBox>,
|
|
490
496
|
clientX: number,
|
|
491
497
|
clientY: number,
|
|
498
|
+
slotMiddlesOf: (index: number) => ReadonlyArray<number>,
|
|
492
499
|
): Target =>
|
|
493
500
|
pipe(
|
|
494
501
|
Array.findFirstIndex(columns, ({ right }) => clientX <= right),
|
|
495
502
|
Option.flatMap((index) => Option.map(Array.get(columns, index), (box) => ({ index, box }))),
|
|
496
503
|
Option.match({
|
|
497
504
|
onNone: () => gapBefore(columns, columns.length),
|
|
498
|
-
onSome: ({ index, box: { id, left, right
|
|
505
|
+
onSome: ({ index, box: { id, left, right } }) => {
|
|
499
506
|
const band = Math.min((right - left) * EDGE_SHARE, EDGE_MAX);
|
|
500
507
|
if (clientX < left + band) {
|
|
501
508
|
return gapBefore(columns, index);
|
|
@@ -504,7 +511,7 @@ export const targetAtPoint = (
|
|
|
504
511
|
} else {
|
|
505
512
|
return Target.Into({
|
|
506
513
|
columnId: id,
|
|
507
|
-
index: Array.filter(
|
|
514
|
+
index: Array.filter(slotMiddlesOf(index), (middle) => middle < clientY).length,
|
|
508
515
|
});
|
|
509
516
|
}
|
|
510
517
|
},
|
package/src/reel/reel.ts
CHANGED
|
@@ -1,4 +1,16 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {
|
|
2
|
+
Array,
|
|
3
|
+
Effect,
|
|
4
|
+
Equal,
|
|
5
|
+
Match,
|
|
6
|
+
Number,
|
|
7
|
+
Option,
|
|
8
|
+
Order,
|
|
9
|
+
Queue,
|
|
10
|
+
Schema,
|
|
11
|
+
Stream,
|
|
12
|
+
String,
|
|
13
|
+
} from "effect";
|
|
2
14
|
import * as Command from "foldkit/command";
|
|
3
15
|
import type { Attribute, Html, HtmlBuilder } from "foldkit/html";
|
|
4
16
|
import { defineMessageUnion } from "foldkit/message";
|
|
@@ -42,6 +54,7 @@ import {
|
|
|
42
54
|
columnOfTarget,
|
|
43
55
|
fill,
|
|
44
56
|
firstPlace,
|
|
57
|
+
isChange,
|
|
45
58
|
isSplitterPresent,
|
|
46
59
|
isTargetPresent,
|
|
47
60
|
landingWords,
|
|
@@ -61,21 +74,22 @@ import {
|
|
|
61
74
|
|
|
62
75
|
// MODEL
|
|
63
76
|
|
|
77
|
+
/** How the keyboard carries what it holds: said on the grip and once it is picked up. */
|
|
78
|
+
const KEYBOARD_MOVES = "Arrow keys move it, space drops it, escape or tab puts it back.";
|
|
79
|
+
|
|
64
80
|
/** The words a reel says. `{pane}` is a pane's label, `{subject}` what moves (the held pane,
|
|
65
81
|
* or a column a key moves, by its first pane), `{where}` a place and `{column}` a column's
|
|
66
82
|
* number. */
|
|
67
83
|
const COPY = {
|
|
68
84
|
paneGrip: "Move {pane}",
|
|
69
|
-
gripHelp:
|
|
70
|
-
"Press space to pick it up. Arrow keys move it, space drops it, escape or tab puts it back.",
|
|
85
|
+
gripHelp: `Press space to pick it up. ${KEYBOARD_MOVES}`,
|
|
71
86
|
widthLabel: "Width of column {column}",
|
|
72
87
|
heightLabel: "Height of {pane}",
|
|
73
88
|
focused: "{pane}, {where}.",
|
|
74
89
|
moved: "{subject} moved to {where}.",
|
|
75
90
|
widened: "Column {column} takes {width}.",
|
|
76
91
|
pickedUp: "Picked up {subject}.",
|
|
77
|
-
pickedUpByKeyboard:
|
|
78
|
-
"Picked up {subject}. Arrow keys move it, space drops it, escape or tab puts it back.",
|
|
92
|
+
pickedUpByKeyboard: `Picked up {subject}. ${KEYBOARD_MOVES}`,
|
|
79
93
|
over: "{subject}, {where}.",
|
|
80
94
|
dropped: "{subject} dropped at {where}.",
|
|
81
95
|
putBack: "{subject} put back where it was.",
|
|
@@ -665,7 +679,7 @@ const requestStep = (
|
|
|
665
679
|
maybeRequest: Option.Option<OutMessage>,
|
|
666
680
|
): UpdateReturn =>
|
|
667
681
|
Option.match(
|
|
668
|
-
Option.filter(maybeRequest, (request) =>
|
|
682
|
+
Option.filter(maybeRequest, (request) => isChange(layout, request)),
|
|
669
683
|
{
|
|
670
684
|
onNone: () => ({ model }),
|
|
671
685
|
onSome: (request) => {
|
|
@@ -823,29 +837,32 @@ const EDGE_SCROLL = 40;
|
|
|
823
837
|
const EDGE_SCROLL_PIXELS_PER_MILLI = 0.8;
|
|
824
838
|
const LONGEST_FRAME_MILLIS = 50;
|
|
825
839
|
|
|
826
|
-
/** A column as the pointer reads it from the page: its edges
|
|
827
|
-
|
|
828
|
-
const columnBoxOf = (element: Element, heldId: string): ColumnBox => {
|
|
840
|
+
/** A column as the pointer reads it from the page: its edges. */
|
|
841
|
+
const columnBoxOf = (element: Element): ColumnBox => {
|
|
829
842
|
const rect = element.getBoundingClientRect();
|
|
830
843
|
return {
|
|
831
844
|
id: element.getAttribute("data-reel-column") ?? "",
|
|
832
845
|
left: rect.left,
|
|
833
846
|
right: rect.right,
|
|
834
|
-
slotMiddles: Array.map(
|
|
835
|
-
Array.filter(
|
|
836
|
-
Array.fromIterable(element.querySelectorAll("[data-reel-slot]")),
|
|
837
|
-
(slot) => slot.getAttribute("data-reel-slot") !== heldId,
|
|
838
|
-
),
|
|
839
|
-
(slot) => {
|
|
840
|
-
const slotRect = slot.getBoundingClientRect();
|
|
841
|
-
return slotRect.top + slotRect.height / 2;
|
|
842
|
-
},
|
|
843
|
-
),
|
|
844
847
|
};
|
|
845
848
|
};
|
|
846
849
|
|
|
850
|
+
/** The middles of a column's slots but the held pane's. */
|
|
851
|
+
const slotMiddlesOf = (element: Element, heldId: string): ReadonlyArray<number> =>
|
|
852
|
+
Array.map(
|
|
853
|
+
Array.filter(
|
|
854
|
+
Array.fromIterable(element.querySelectorAll("[data-reel-slot]")),
|
|
855
|
+
(slot) => slot.getAttribute("data-reel-slot") !== heldId,
|
|
856
|
+
),
|
|
857
|
+
(slot) => {
|
|
858
|
+
const slotRect = slot.getBoundingClientRect();
|
|
859
|
+
return slotRect.top + slotRect.height / 2;
|
|
860
|
+
},
|
|
861
|
+
);
|
|
862
|
+
|
|
847
863
|
/** Where a pointer at (`clientX`, `clientY`) would drop the pane `paneId` in the reel
|
|
848
|
-
* `reelHook` names, read from where its columns and
|
|
864
|
+
* `reelHook` names, read from where its columns sit and the slots of the one it is over: none
|
|
865
|
+
* outside the reel. */
|
|
849
866
|
const pointerTarget = (
|
|
850
867
|
reelHook: string,
|
|
851
868
|
paneId: string,
|
|
@@ -862,14 +879,15 @@ const pointerTarget = (
|
|
|
862
879
|
clientY <= rect.bottom
|
|
863
880
|
);
|
|
864
881
|
}),
|
|
865
|
-
(view) =>
|
|
866
|
-
|
|
867
|
-
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
|
|
871
|
-
|
|
872
|
-
)
|
|
882
|
+
(view) => {
|
|
883
|
+
const columns = Array.fromIterable(view.querySelectorAll("[data-reel-column]"));
|
|
884
|
+
return targetAtPoint(Array.map(columns, columnBoxOf), clientX, clientY, (index) =>
|
|
885
|
+
Option.match(Array.get(columns, index), {
|
|
886
|
+
onNone: () => [],
|
|
887
|
+
onSome: (column) => slotMiddlesOf(column, paneId),
|
|
888
|
+
}),
|
|
889
|
+
);
|
|
890
|
+
},
|
|
873
891
|
);
|
|
874
892
|
|
|
875
893
|
const PointerHold = Schema.Struct({
|
|
@@ -885,13 +903,60 @@ const pointerAt = (hold: PointerHold, event: PointerEvent): PointerAt => ({
|
|
|
885
903
|
maybeTarget: pointerTarget(hold.reelHook, hold.paneId, event.clientX, event.clientY),
|
|
886
904
|
});
|
|
887
905
|
|
|
888
|
-
/** The pressing pointer's moves, release and cancel; another pointer's are not the reel's.
|
|
906
|
+
/** The pressing pointer's moves, release and cancel; another pointer's are not the reel's. A
|
|
907
|
+
* pointer reports moves faster than the page draws, and each reads the reel's layout, so its
|
|
908
|
+
* moves are read once a frame, at the latest; a release first reads the move not read yet,
|
|
909
|
+
* so the pane is carried as far as the pointer went, and a cancel drops it. */
|
|
889
910
|
const pointerMessages = (hold: PointerHold): Stream.Stream<Message> =>
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
911
|
+
Stream.callback<Message>((queue) =>
|
|
912
|
+
Effect.acquireRelease(
|
|
913
|
+
Effect.sync(() => {
|
|
914
|
+
const state = { maybeMove: Option.none<PointerEvent>(), frame: 0 };
|
|
915
|
+
const offer = (message: Message): void => {
|
|
916
|
+
Queue.offerUnsafe(queue, message);
|
|
917
|
+
};
|
|
918
|
+
const readMove = (): void => {
|
|
919
|
+
cancelAnimationFrame(state.frame);
|
|
920
|
+
Option.map(state.maybeMove, (event) =>
|
|
921
|
+
offer(Message.MovedPointer(pointerAt(hold, event))),
|
|
922
|
+
);
|
|
923
|
+
state.maybeMove = Option.none();
|
|
924
|
+
};
|
|
925
|
+
const onMove = (event: PointerEvent): void => {
|
|
926
|
+
if (event.pointerId === hold.pointerId) {
|
|
927
|
+
if (Option.isNone(state.maybeMove)) {
|
|
928
|
+
state.frame = requestAnimationFrame(readMove);
|
|
929
|
+
}
|
|
930
|
+
state.maybeMove = Option.some(event);
|
|
931
|
+
}
|
|
932
|
+
};
|
|
933
|
+
const onUp = (event: PointerEvent): void => {
|
|
934
|
+
if (event.pointerId === hold.pointerId) {
|
|
935
|
+
readMove();
|
|
936
|
+
offer(Message.ReleasedPointer(pointerAt(hold, event)));
|
|
937
|
+
}
|
|
938
|
+
};
|
|
939
|
+
const onCancel = (event: PointerEvent): void => {
|
|
940
|
+
if (event.pointerId === hold.pointerId) {
|
|
941
|
+
cancelAnimationFrame(state.frame);
|
|
942
|
+
state.maybeMove = Option.none();
|
|
943
|
+
offer(Message.CancelledPointer());
|
|
944
|
+
}
|
|
945
|
+
};
|
|
946
|
+
document.addEventListener("pointermove", onMove);
|
|
947
|
+
document.addEventListener("pointerup", onUp);
|
|
948
|
+
document.addEventListener("pointercancel", onCancel);
|
|
949
|
+
return { state, onMove, onUp, onCancel };
|
|
950
|
+
}),
|
|
951
|
+
({ state, onMove, onUp, onCancel }) =>
|
|
952
|
+
Effect.sync(() => {
|
|
953
|
+
cancelAnimationFrame(state.frame);
|
|
954
|
+
document.removeEventListener("pointermove", onMove);
|
|
955
|
+
document.removeEventListener("pointerup", onUp);
|
|
956
|
+
document.removeEventListener("pointercancel", onCancel);
|
|
957
|
+
}),
|
|
958
|
+
),
|
|
959
|
+
);
|
|
895
960
|
|
|
896
961
|
/** Which way a pointer at `at` scrolls a box from `low` to `high`: towards the edge it is near. */
|
|
897
962
|
const towards = (at: number, low: number, high: number): number => {
|
|
@@ -1224,33 +1289,32 @@ const reelSplitterAttributes = (
|
|
|
1224
1289
|
h,
|
|
1225
1290
|
);
|
|
1226
1291
|
|
|
1227
|
-
|
|
1228
|
-
|
|
1229
|
-
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1292
|
+
/** The pane a drop into `column` is marked on, and the class that marks it: the pane it would
|
|
1293
|
+
* land before, or the last one when it would land at the end, the carried pane left out. */
|
|
1294
|
+
const dropMarkOf = (
|
|
1295
|
+
info: ReelInfo,
|
|
1296
|
+
column: Column,
|
|
1297
|
+
): Option.Option<Readonly<{ paneId: string; markClass: string }>> =>
|
|
1298
|
+
Option.flatMap(info.maybeMark, (mark) => {
|
|
1299
|
+
if (mark._tag !== "Into" || mark.columnId !== column.id) {
|
|
1300
|
+
return Option.none();
|
|
1301
|
+
}
|
|
1302
|
+
const others = Array.filter(
|
|
1303
|
+
column.panes,
|
|
1304
|
+
(pane) => !Option.contains(info.maybeCarriedId, pane.id),
|
|
1305
|
+
);
|
|
1306
|
+
return Option.orElse(
|
|
1307
|
+
Option.map(Array.get(others, mark.index), ({ id }) => ({
|
|
1308
|
+
paneId: id,
|
|
1309
|
+
markClass: " drop-before",
|
|
1310
|
+
})),
|
|
1311
|
+
() =>
|
|
1238
1312
|
Option.map(
|
|
1239
|
-
Option.filter(Array.
|
|
1240
|
-
() => " drop-
|
|
1313
|
+
Option.filter(Array.last(others), () => mark.index >= others.length),
|
|
1314
|
+
({ id }) => ({ paneId: id, markClass: " drop-after" }),
|
|
1241
1315
|
),
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
Option.filter(
|
|
1245
|
-
Array.last(others),
|
|
1246
|
-
(pane) => mark.index >= others.length && pane.id === paneId,
|
|
1247
|
-
),
|
|
1248
|
-
() => " drop-after",
|
|
1249
|
-
),
|
|
1250
|
-
);
|
|
1251
|
-
}),
|
|
1252
|
-
{ onNone: () => "", onSome: (mark) => mark },
|
|
1253
|
-
);
|
|
1316
|
+
);
|
|
1317
|
+
});
|
|
1254
1318
|
|
|
1255
1319
|
const isGapMarked = (info: ReelInfo, maybeBeforeColumnId: Option.Option<string>): boolean =>
|
|
1256
1320
|
Option.exists(
|
|
@@ -1259,14 +1323,18 @@ const isGapMarked = (info: ReelInfo, maybeBeforeColumnId: Option.Option<string>)
|
|
|
1259
1323
|
);
|
|
1260
1324
|
|
|
1261
1325
|
const slotView = (
|
|
1262
|
-
info: ReelInfo,
|
|
1263
1326
|
anchors: ReadonlyMap<string, string>,
|
|
1264
|
-
|
|
1327
|
+
maybeDropMark: Option.Option<Readonly<{ paneId: string; markClass: string }>>,
|
|
1265
1328
|
pane: Pane,
|
|
1266
1329
|
h: HtmlBuilder<Message>,
|
|
1267
1330
|
): Html =>
|
|
1268
1331
|
h.keyed("div")(pane.id, [
|
|
1269
|
-
h.Class(
|
|
1332
|
+
h.Class(
|
|
1333
|
+
`reel-slot${Option.match(
|
|
1334
|
+
Option.filter(maybeDropMark, ({ paneId }) => paneId === pane.id),
|
|
1335
|
+
{ onNone: () => "", onSome: ({ markClass }) => markClass },
|
|
1336
|
+
)}`,
|
|
1337
|
+
),
|
|
1270
1338
|
h.DataAttribute("reel-slot", pane.id),
|
|
1271
1339
|
h.Style({ "--reel-weight": `${pane.weight}`, "--reel-anchor": anchors.get(pane.id) ?? "none" }),
|
|
1272
1340
|
]);
|
|
@@ -1293,13 +1361,14 @@ const columnView = (
|
|
|
1293
1361
|
): ReadonlyArray<Html> => {
|
|
1294
1362
|
const { model, layout } = info;
|
|
1295
1363
|
const total = Number.sumAll(Array.map(column.panes, (pane) => pane.weight));
|
|
1364
|
+
const maybeDropMark = dropMarkOf(info, column);
|
|
1296
1365
|
const stack = Array.flatMap(column.panes, (pane, row) => [
|
|
1297
1366
|
...Array.fromOption(
|
|
1298
1367
|
Option.map(Array.get(column.panes, row - 1), (above) =>
|
|
1299
1368
|
splitView(model, above, total > 0 ? above.weight / total : 0, h),
|
|
1300
1369
|
),
|
|
1301
1370
|
),
|
|
1302
|
-
slotView(
|
|
1371
|
+
slotView(anchors, maybeDropMark, pane, h),
|
|
1303
1372
|
]);
|
|
1304
1373
|
const maybeNextId = Option.map(Array.get(layout, index + 1), ({ id }) => id);
|
|
1305
1374
|
|
package/src/scheme/scheme.ts
CHANGED
|
@@ -21,7 +21,7 @@ export const toPinnedValue = (scheme: Scheme): Option.Option<string> =>
|
|
|
21
21
|
);
|
|
22
22
|
|
|
23
23
|
/** The scheme a `data-scheme` value pins. Anything else, or nothing, is `System`. */
|
|
24
|
-
export const fromPinnedValue = (value: string | null
|
|
24
|
+
export const fromPinnedValue = (value: string | null): Scheme =>
|
|
25
25
|
Match.value(value).pipe(
|
|
26
26
|
Match.withReturnType<Scheme>(),
|
|
27
27
|
Match.when("light", () => "Paper"),
|
|
@@ -2,7 +2,6 @@ import { Button as UiButton } from "@foldkit/ui";
|
|
|
2
2
|
import { Array } from "effect";
|
|
3
3
|
import type { Html, HtmlBuilder } from "foldkit/html";
|
|
4
4
|
|
|
5
|
-
import type { Attributes } from "../attributes/attributes.ts";
|
|
6
5
|
import { pressedAttribute } from "../button/button.ts";
|
|
7
6
|
import * as Glyph from "../glyph/glyph.ts";
|
|
8
7
|
import type { GlyphName } from "../glyph/glyphs.ts";
|
|
@@ -24,8 +23,6 @@ export type ViewConfig<Value extends string, Message> = Readonly<{
|
|
|
24
23
|
options: ReadonlyArray<Segment<Value>>;
|
|
25
24
|
value: Value;
|
|
26
25
|
onSelect: (value: Value) => Message;
|
|
27
|
-
/** Further attributes for the group, never a class. */
|
|
28
|
-
attributes?: Attributes<Message>;
|
|
29
26
|
}>;
|
|
30
27
|
|
|
31
28
|
const segmentView = <Value extends string, Message>(
|
|
@@ -62,12 +59,7 @@ export const view = <Value extends string, Message>(
|
|
|
62
59
|
const isGlyphs = Array.every(config.options, (option) => option.glyph !== undefined);
|
|
63
60
|
|
|
64
61
|
return h.div(
|
|
65
|
-
[
|
|
66
|
-
...(config.attributes ?? []),
|
|
67
|
-
h.Role("group"),
|
|
68
|
-
h.AriaLabel(config.label),
|
|
69
|
-
h.Class(isGlyphs ? "switch" : "nav"),
|
|
70
|
-
],
|
|
62
|
+
[h.Role("group"), h.AriaLabel(config.label), h.Class(isGlyphs ? "switch" : "nav")],
|
|
71
63
|
Array.map(config.options, (option) => segmentView(config, option, h)),
|
|
72
64
|
);
|
|
73
65
|
};
|
|
@@ -2,7 +2,7 @@ import { Select as UiSelect } from "@foldkit/ui";
|
|
|
2
2
|
import { Array } from "effect";
|
|
3
3
|
import type { Html, HtmlBuilder } from "foldkit/html";
|
|
4
4
|
|
|
5
|
-
import {
|
|
5
|
+
import { controlAttributes, errorView, helpView } from "../field/field.ts";
|
|
6
6
|
|
|
7
7
|
// VIEW
|
|
8
8
|
|
|
@@ -78,19 +78,12 @@ export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Messag
|
|
|
78
78
|
[h.Class("field")],
|
|
79
79
|
[
|
|
80
80
|
h.label(groups.label, [label]),
|
|
81
|
-
// NOTE: @foldkit/ui gap: Select has no required option, so the wrapper says it.
|
|
82
81
|
h.select(
|
|
83
|
-
[
|
|
84
|
-
...groups.select,
|
|
85
|
-
...describedByError(id, error, help !== undefined, h),
|
|
86
|
-
...(isRequired ? [h.AriaRequired(true)] : []),
|
|
87
|
-
],
|
|
82
|
+
[...groups.select, ...controlAttributes(id, error, help, isRequired, h)],
|
|
88
83
|
Array.map(options, entryView(h)),
|
|
89
84
|
),
|
|
90
85
|
...errorView(id, error, h),
|
|
91
|
-
...(help
|
|
92
|
-
? []
|
|
93
|
-
: [h.p([...groups.description, h.Class("field-help")], [help])]),
|
|
86
|
+
...helpView(help, groups.description, h),
|
|
94
87
|
],
|
|
95
88
|
),
|
|
96
89
|
},
|
package/src/sortable/index.ts
CHANGED
package/src/sortable/sortable.ts
CHANGED
|
@@ -14,8 +14,9 @@ import * as Button from "../button/button.ts";
|
|
|
14
14
|
import {
|
|
15
15
|
LEFT_BUTTON,
|
|
16
16
|
activatedKeyboardDrag,
|
|
17
|
+
canLift,
|
|
17
18
|
cancelledDrag,
|
|
18
|
-
|
|
19
|
+
escapeSubscriptions,
|
|
19
20
|
insertionIndex,
|
|
20
21
|
isCarrying,
|
|
21
22
|
markDragging,
|
|
@@ -25,6 +26,7 @@ import {
|
|
|
25
26
|
pointerMessages as dragMessages,
|
|
26
27
|
pressedDraggable,
|
|
27
28
|
releasedPointer,
|
|
29
|
+
settle as settleDrag,
|
|
28
30
|
} from "../drag/drag.ts";
|
|
29
31
|
import * as Glyph from "../glyph/glyph.ts";
|
|
30
32
|
import * as IconButton from "../iconButton/iconButton.ts";
|
|
@@ -38,15 +40,16 @@ import { prefixKeys } from "../subscription/prefixKeys.ts";
|
|
|
38
40
|
export const Item = Schema.Struct({ id: Schema.String, label: Schema.String });
|
|
39
41
|
export type Item = typeof Item.Type;
|
|
40
42
|
|
|
43
|
+
/** How the keyboard carries what it holds: said on the grip and once it is picked up. */
|
|
44
|
+
const KEYBOARD_MOVES = "Arrow keys move it one place, space drops it, escape cancels.";
|
|
45
|
+
|
|
41
46
|
/** The words a sortable says. `{item}` stands for the item's label, `{position}` and `{total}`
|
|
42
47
|
* for where it is, counted from one. */
|
|
43
48
|
const COPY = {
|
|
44
49
|
gripLabel: "Move {item}",
|
|
45
|
-
gripHelp:
|
|
46
|
-
"Press space to pick the item up. Arrow keys move it one place, space drops it, escape cancels.",
|
|
50
|
+
gripHelp: `Press space to pick the item up. ${KEYBOARD_MOVES}`,
|
|
47
51
|
pickedUp: "Picked up {item}.",
|
|
48
|
-
pickedUpByKeyboard:
|
|
49
|
-
"Picked up {item}, {position} of {total}. Arrow keys move it one place, space drops it, escape cancels.",
|
|
52
|
+
pickedUpByKeyboard: `Picked up {item}, {position} of {total}. ${KEYBOARD_MOVES}`,
|
|
50
53
|
over: "{item}, {position} of {total}.",
|
|
51
54
|
dropped: "{item} dropped at {position} of {total}.",
|
|
52
55
|
putBack: "{item} put back where it was.",
|
|
@@ -150,10 +153,6 @@ export const init = (config: InitConfig): Model => ({
|
|
|
150
153
|
/** The list's id, unique in the document. */
|
|
151
154
|
const id = (model: Model): string => model.dragAndDrop.id;
|
|
152
155
|
|
|
153
|
-
/** Whether an item is pressed or held. */
|
|
154
|
-
export const isHolding = (model: Model): boolean =>
|
|
155
|
-
Option.isSome(UiDragAndDrop.maybeDraggedItemId(model.dragAndDrop));
|
|
156
|
-
|
|
157
156
|
// DOM HOOKS
|
|
158
157
|
|
|
159
158
|
const itemHook = (model: Model, itemId: string): string => `${id(model)}/${itemId}`;
|
|
@@ -204,10 +203,7 @@ const placementOf = (model: Model, held: Held, rawIndex: number): Option.Option<
|
|
|
204
203
|
itemId: held.item.id,
|
|
205
204
|
fromIndex,
|
|
206
205
|
toIndex,
|
|
207
|
-
maybeAfterItemId: Option.map(
|
|
208
|
-
toIndex > 0 ? Array.get(others, toIndex - 1) : Option.none(),
|
|
209
|
-
(item) => item.id,
|
|
210
|
-
),
|
|
206
|
+
maybeAfterItemId: Option.map(Array.get(others, toIndex - 1), (item) => item.id),
|
|
211
207
|
maybeBeforeItemId: Option.map(Array.get(others, toIndex), (item) => item.id),
|
|
212
208
|
});
|
|
213
209
|
});
|
|
@@ -283,38 +279,23 @@ const moveText = (
|
|
|
283
279
|
/** Announces the step a DragAndDrop update took and forgets the held item once it is down. */
|
|
284
280
|
const settle =
|
|
285
281
|
(previous: Model, putBack = COPY.putBack) =>
|
|
286
|
-
(dragAndDropUpdate: UpdateReturn): UpdateReturn =>
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
282
|
+
(dragAndDropUpdate: UpdateReturn): UpdateReturn =>
|
|
283
|
+
settleDrag(
|
|
284
|
+
dragAndDropUpdate,
|
|
285
|
+
Option.map(
|
|
286
|
+
Option.flatMap(previous.maybeHeld, (held) =>
|
|
287
|
+
moveText(
|
|
288
|
+
held,
|
|
289
|
+
previous,
|
|
290
|
+
dragAndDropUpdate.model,
|
|
291
|
+
Option.fromNullishOr(dragAndDropUpdate.outMessage),
|
|
292
|
+
putBack,
|
|
293
|
+
),
|
|
294
|
+
),
|
|
295
|
+
(text) => AnnounceReorder({ text }),
|
|
298
296
|
),
|
|
299
297
|
);
|
|
300
298
|
|
|
301
|
-
return Update.withOutMessage(
|
|
302
|
-
{
|
|
303
|
-
model: nextModel,
|
|
304
|
-
commands: [
|
|
305
|
-
...(dragAndDropUpdate.commands ?? []),
|
|
306
|
-
...Array.fromOption(Option.map(maybeText, (text) => AnnounceReorder({ text }))),
|
|
307
|
-
],
|
|
308
|
-
},
|
|
309
|
-
dragAndDropUpdate.outMessage,
|
|
310
|
-
);
|
|
311
|
-
};
|
|
312
|
-
|
|
313
|
-
// NOTE: @foldkit/ui gap: DragAndDrop's pointer resolves a drop along one axis, which a
|
|
314
|
-
// wrapping grid cannot use, so the list reads the pointer itself and hands DragAndDrop its
|
|
315
|
-
// moves and release.
|
|
316
|
-
const canLift = (model: Model): boolean => model.isMovable && phaseOf(model.dragAndDrop) === "Idle";
|
|
317
|
-
|
|
318
299
|
/** The item `itemId` and its place, when the list may lift it now. */
|
|
319
300
|
const liftable = (
|
|
320
301
|
model: Model,
|
|
@@ -322,8 +303,9 @@ const liftable = (
|
|
|
322
303
|
itemId: string,
|
|
323
304
|
): Option.Option<Readonly<{ index: number; item: Item }>> =>
|
|
324
305
|
Option.filter(
|
|
325
|
-
Option.
|
|
326
|
-
|
|
306
|
+
Option.map(
|
|
307
|
+
Array.findFirstWithIndex(items, (item) => item.id === itemId),
|
|
308
|
+
([item, index]) => ({ index, item }),
|
|
327
309
|
),
|
|
328
310
|
() => canLift(model),
|
|
329
311
|
);
|
|
@@ -441,6 +423,9 @@ export const setMovable = (model: Model, isMovable: boolean): UpdateReturn => {
|
|
|
441
423
|
|
|
442
424
|
// SUBSCRIPTION
|
|
443
425
|
|
|
426
|
+
// NOTE: @foldkit/ui gap: DragAndDrop's pointer resolves a drop along one axis, which a
|
|
427
|
+
// wrapping grid cannot use, so the list reads the pointer itself and hands DragAndDrop its
|
|
428
|
+
// moves and release.
|
|
444
429
|
/** Where a pointer at this point would drop the held item in the list `listId`: none when the
|
|
445
430
|
* pointer is not over it. */
|
|
446
431
|
const pointerIndex = (
|
|
@@ -480,7 +465,7 @@ const pointerMessages = (hold: PointerHold): Stream.Stream<Message> =>
|
|
|
480
465
|
cancelled: () => Message.CancelledPointer(),
|
|
481
466
|
});
|
|
482
467
|
|
|
483
|
-
const
|
|
468
|
+
const pointerSubscriptions = Subscription.make<Model, Message>()((entry) => ({
|
|
484
469
|
pointer: entry(
|
|
485
470
|
{ maybeHold: Schema.Option(PointerHold) },
|
|
486
471
|
{
|
|
@@ -506,14 +491,6 @@ const ownSubscriptions = Subscription.make<Model, Message>()((entry) => ({
|
|
|
506
491
|
}),
|
|
507
492
|
},
|
|
508
493
|
),
|
|
509
|
-
escape: entry(
|
|
510
|
-
{ isHolding: Schema.Boolean },
|
|
511
|
-
{
|
|
512
|
-
modelToDependencies: (model) => ({ isHolding: isHolding(model) }),
|
|
513
|
-
dependenciesToStream: ({ isHolding: isHoldingNow }) =>
|
|
514
|
-
isHoldingNow ? escapeKeys(Message.PressedEscape()) : Stream.empty,
|
|
515
|
-
},
|
|
516
|
-
),
|
|
517
494
|
}));
|
|
518
495
|
|
|
519
496
|
// NOTE: @foldkit/ui gap: DragAndDrop's Subscription keys are fixed, so two of its users on a
|
|
@@ -527,7 +504,11 @@ const dragSubscriptions = Subscription.lift({
|
|
|
527
504
|
toParentMessage: toDragAndDropMessage,
|
|
528
505
|
});
|
|
529
506
|
|
|
530
|
-
const sortableSubscriptions = Subscription.aggregate(
|
|
507
|
+
const sortableSubscriptions = Subscription.aggregate(
|
|
508
|
+
pointerSubscriptions,
|
|
509
|
+
escapeSubscriptions<Model, Message>(Message.PressedEscape()),
|
|
510
|
+
dragSubscriptions,
|
|
511
|
+
);
|
|
531
512
|
|
|
532
513
|
/** A held item follows the pointer, the arrows and Space move and drop it, Escape puts it back
|
|
533
514
|
* and the page scrolls near its edges, from listeners that run only while an item is pressed
|
package/src/stack/stack.ts
CHANGED
|
@@ -4,7 +4,7 @@ import type { Html, HtmlBuilder } from "foldkit/html";
|
|
|
4
4
|
// SPACE
|
|
5
5
|
|
|
6
6
|
/** `S`: things in one line (8px). `M`: the parts of one body (14px). `L`: whole surfaces,
|
|
7
|
-
* panel to panel (24px).
|
|
7
|
+
* panel to panel (24px). */
|
|
8
8
|
export const Space = Schema.Literals(["S", "M", "L"]);
|
|
9
9
|
export type Space = typeof Space.Type;
|
|
10
10
|
|