@gjermundgaraba-designs/pixel-paper 0.14.1 → 0.16.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 +7 -6
- package/dist/src/card/card.d.ts +10 -5
- package/dist/src/card/card.d.ts.map +1 -1
- package/dist/src/card/card.js +4 -4
- package/dist/src/commandPalette/commandPalette.d.ts +0 -4
- package/dist/src/commandPalette/commandPalette.d.ts.map +1 -1
- package/dist/src/confirmDialog/confirmDialog.d.ts +17 -24
- package/dist/src/confirmDialog/confirmDialog.d.ts.map +1 -1
- package/dist/src/confirmDialog/confirmDialog.js +16 -22
- package/dist/src/confirmDialog/index.d.ts +1 -1
- package/dist/src/confirmDialog/index.d.ts.map +1 -1
- package/dist/src/confirmDialog/index.js +1 -1
- package/dist/src/dialog/dialog.d.ts +14 -20
- package/dist/src/dialog/dialog.d.ts.map +1 -1
- package/dist/src/dialog/dialog.js +10 -23
- package/dist/src/dialog/fold.d.ts +9 -1
- package/dist/src/dialog/fold.d.ts.map +1 -1
- package/dist/src/dialog/fold.js +9 -5
- package/dist/src/dialog/index.d.ts +2 -2
- package/dist/src/dialog/index.d.ts.map +1 -1
- package/dist/src/dialog/index.js +1 -1
- package/dist/src/keyHelp/keyHelp.d.ts +0 -4
- package/dist/src/keyHelp/keyHelp.d.ts.map +1 -1
- package/dist/src/panel/panel.d.ts +3 -0
- package/dist/src/panel/panel.d.ts.map +1 -1
- package/dist/src/panel/panel.js +2 -1
- package/dist/src/sortable/sortable.d.ts +19 -7
- package/dist/src/sortable/sortable.d.ts.map +1 -1
- package/dist/src/sortable/sortable.js +30 -21
- package/package.json +1 -1
- package/src/card/card.ts +12 -6
- package/src/confirmDialog/confirmDialog.ts +20 -27
- package/src/confirmDialog/index.ts +0 -2
- package/src/dialog/dialog.ts +23 -30
- package/src/dialog/fold.ts +25 -6
- package/src/dialog/index.ts +1 -3
- package/src/panel/panel.ts +5 -1
- package/src/pixel-paper.css +87 -10
- package/src/sortable/sortable.ts +55 -23
package/src/dialog/dialog.ts
CHANGED
|
@@ -13,11 +13,9 @@ import { foldDialog } from "./fold.ts";
|
|
|
13
13
|
|
|
14
14
|
// MODEL
|
|
15
15
|
|
|
16
|
-
/** Schema for a pixel-paper dialog: the `@foldkit/ui` Dialog it is built on
|
|
17
|
-
* ways out are held (`setCloseHeld`). */
|
|
16
|
+
/** Schema for a pixel-paper dialog: the `@foldkit/ui` Dialog it is built on. */
|
|
18
17
|
export const Model = Schema.Struct({
|
|
19
18
|
dialog: UiDialog.Model,
|
|
20
|
-
isCloseHeld: Schema.Boolean,
|
|
21
19
|
});
|
|
22
20
|
export type Model = typeof Model.Type;
|
|
23
21
|
|
|
@@ -56,7 +54,6 @@ export type InitConfig = Readonly<{ id: string }>;
|
|
|
56
54
|
/** Creates a closed dialog. */
|
|
57
55
|
export const init = (config: InitConfig): Model => ({
|
|
58
56
|
dialog: UiDialog.init({ id: config.id }),
|
|
59
|
-
isCloseHeld: false,
|
|
60
57
|
});
|
|
61
58
|
|
|
62
59
|
// ACCESSORS
|
|
@@ -67,9 +64,6 @@ export const id = (model: Model): string => model.dialog.id;
|
|
|
67
64
|
/** Whether the dialog is open. */
|
|
68
65
|
export const isOpen = (model: Model): boolean => model.dialog.isOpen;
|
|
69
66
|
|
|
70
|
-
/** Whether the dialog's ways out are held (`setCloseHeld`). */
|
|
71
|
-
export const isCloseHeld = (model: Model): boolean => model.isCloseHeld;
|
|
72
|
-
|
|
73
67
|
// UPDATE
|
|
74
68
|
|
|
75
69
|
const toOutMessage = UiDialog.OutMessage.match<OutMessage | undefined>({
|
|
@@ -85,39 +79,38 @@ const dialogFold = foldDialog(UiDialog, {
|
|
|
85
79
|
toParentOutMessage: toOutMessage,
|
|
86
80
|
});
|
|
87
81
|
|
|
82
|
+
/** What a dialog's update reads from its parent as a Message arrives. */
|
|
83
|
+
export type UpdateContext = Readonly<{
|
|
84
|
+
/** The reader may not walk away from the work running now (a save whose failure must land
|
|
85
|
+
* in this dialog): Escape, a press on the veil and a control given the `closeButton` group
|
|
86
|
+
* change nothing and report no `Closed`. Read it from the parent's own state as the
|
|
87
|
+
* Message arrives (`isSaving(model)`), so it holds exactly while the work runs, however
|
|
88
|
+
* soon after the start a key comes. */
|
|
89
|
+
isCloseHeld: boolean;
|
|
90
|
+
}>;
|
|
91
|
+
|
|
92
|
+
const FREE: UpdateContext = { isCloseHeld: false };
|
|
93
|
+
|
|
88
94
|
// NOTE: @foldkit/ui gap: Dialog has no way to hold a close. Escape, a press on the veil and a
|
|
89
95
|
// control given the `closeButton` group all send `RequestedClose`, so a held dialog reads it
|
|
90
96
|
// by its tag and leaves it out.
|
|
91
|
-
const isHeldClose = (
|
|
92
|
-
|
|
97
|
+
const isHeldClose = (context: UpdateContext, message: UiDialog.Message): boolean =>
|
|
98
|
+
context.isCloseHeld && message._tag === "RequestedClose";
|
|
93
99
|
|
|
94
100
|
/** Processes a dialog Message and returns the next Model, Commands and OutMessage. */
|
|
95
|
-
export const update = (model: Model, message: Message) =>
|
|
101
|
+
export const update = (model: Model, message: Message, context: UpdateContext = FREE) =>
|
|
96
102
|
Message.match<Update.ReturnWithOutMessage<Model, Message, OutMessage>>(message, {
|
|
97
103
|
GotDialogMessage: ({ message: dialogMessage }) =>
|
|
98
|
-
isHeldClose(
|
|
104
|
+
isHeldClose(context, dialogMessage) ? { model } : dialogFold.update(model, dialogMessage),
|
|
99
105
|
PressedActionKey: () => ({ model }),
|
|
100
106
|
});
|
|
101
107
|
|
|
102
|
-
/** Opens the dialog
|
|
103
|
-
*
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
/** Closes the dialog without reporting `Closed`, and lets go of a hold. */
|
|
109
|
-
export const close = (model: Model): Update.Return<Model, Message> =>
|
|
110
|
-
dialogFold.close(modifyFields(model, { isCloseHeld: () => false }));
|
|
111
|
-
|
|
112
|
-
/** Holds the dialog's ways out, or lets them go: while held, Escape, a press on the veil and
|
|
113
|
-
* a control given the `closeButton` group leave it open and report no `Closed`, so the
|
|
114
|
-
* result of work the reader may not walk away from (a save whose failure must land here)
|
|
115
|
-
* lands in it. Hold it in the update that starts the work, and let go in the update where
|
|
116
|
-
* the work fails, or the reader is shut in with the failure; `close` lets go once it lands,
|
|
117
|
-
* and `open` opens it free.
|
|
118
|
-
* Hold the card's own close controls too (`Card.formView`'s `isBusy`). */
|
|
119
|
-
export const setCloseHeld = (model: Model, isHeld: boolean): Model =>
|
|
120
|
-
modifyFields(model, { isCloseHeld: () => isHeld });
|
|
108
|
+
/** Opens the dialog: focus moves to the element given the `initialFocus` group, else to the
|
|
109
|
+
* first control, and returns to where it was when the dialog closes. */
|
|
110
|
+
export const open = (model: Model): Update.Return<Model, Message> => dialogFold.open(model);
|
|
111
|
+
|
|
112
|
+
/** Closes the dialog without reporting `Closed`. */
|
|
113
|
+
export const close = (model: Model): Update.Return<Model, Message> => dialogFold.close(model);
|
|
121
114
|
|
|
122
115
|
// ACTION BAR KEYS
|
|
123
116
|
|
package/src/dialog/fold.ts
CHANGED
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
import { Option } from "effect";
|
|
2
2
|
import * as Update from "foldkit/update";
|
|
3
3
|
|
|
4
|
+
import type { UpdateContext } from "./dialog.ts";
|
|
5
|
+
|
|
4
6
|
// FOLD
|
|
5
7
|
|
|
6
8
|
type ChildReturn<ChildModel, ChildMessage, ChildOutMessage> = Update.ReturnWithOutMessage<
|
|
@@ -15,6 +17,7 @@ type DialogChild<ChildModel, ChildMessage, ChildOutMessage> = Readonly<{
|
|
|
15
17
|
update: (
|
|
16
18
|
model: ChildModel,
|
|
17
19
|
message: ChildMessage,
|
|
20
|
+
context: UpdateContext,
|
|
18
21
|
) => ChildReturn<ChildModel, ChildMessage, ChildOutMessage>;
|
|
19
22
|
open: (model: ChildModel) => ChildReturn<ChildModel, ChildMessage, ChildOutMessage>;
|
|
20
23
|
close: (model: ChildModel) => ChildReturn<ChildModel, ChildMessage, ChildOutMessage>;
|
|
@@ -36,6 +39,10 @@ type DialogFoldConfig<
|
|
|
36
39
|
* veil, a close button). Without it the dialog's OutMessages report nothing: a parent
|
|
37
40
|
* that reads `isOpen` when it needs to. */
|
|
38
41
|
toParentOutMessage?: (outMessage: ChildOutMessage) => ParentOutMessage | undefined;
|
|
42
|
+
/** Whether the dialog's ways out are held, read from the parent's own state as each
|
|
43
|
+
* Message arrives (`(model) => model.isSaving`). Free when not given. See
|
|
44
|
+
* `Dialog.UpdateContext`. */
|
|
45
|
+
isCloseHeld?: (model: ParentModel) => boolean;
|
|
39
46
|
}>;
|
|
40
47
|
|
|
41
48
|
/** The dialog folded into its parent: a dialog Message, and opening and closing it. */
|
|
@@ -44,6 +51,14 @@ type DialogFold<ParentModel, ParentMessage, ParentOutMessage, ChildMessage> = Re
|
|
|
44
51
|
model: ParentModel,
|
|
45
52
|
message: ChildMessage,
|
|
46
53
|
) => Update.ReturnWithOutMessage<ParentModel, ParentMessage, ParentOutMessage>;
|
|
54
|
+
/** The update a parent that is itself a dialog (ConfirmDialog) runs with the hold its own
|
|
55
|
+
* parent gave it. */
|
|
56
|
+
updateWith: (
|
|
57
|
+
context: UpdateContext,
|
|
58
|
+
) => (
|
|
59
|
+
model: ParentModel,
|
|
60
|
+
message: ChildMessage,
|
|
61
|
+
) => Update.ReturnWithOutMessage<ParentModel, ParentMessage, ParentOutMessage>;
|
|
47
62
|
open: (model: ParentModel) => Update.Return<ParentModel, ParentMessage>;
|
|
48
63
|
close: (model: ParentModel) => Update.Return<ParentModel, ParentMessage>;
|
|
49
64
|
}>;
|
|
@@ -98,14 +113,18 @@ export const foldDialog = <
|
|
|
98
113
|
// `RequestedOpen`, which reports `Opened`), and so can its update, so each fold needs a
|
|
99
114
|
// step for an OutMessage. This one keeps the parent's Model: a step reports nothing.
|
|
100
115
|
const foldOutMessage = () => (model: ParentModel) => ({ model });
|
|
101
|
-
const { toParentOutMessage } = config;
|
|
102
|
-
const
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
116
|
+
const { toParentOutMessage, isCloseHeld = () => false } = config;
|
|
117
|
+
const updateWith = (context: UpdateContext) => {
|
|
118
|
+
const childUpdate = (model: ChildModel, message: ChildMessage) =>
|
|
119
|
+
child.update(model, message, context);
|
|
120
|
+
return toParentOutMessage === undefined
|
|
121
|
+
? Update.foldChild({ update: childUpdate, ...lift, foldOutMessage })
|
|
122
|
+
: Update.foldChild({ update: childUpdate, ...lift, toParentOutMessage });
|
|
123
|
+
};
|
|
106
124
|
|
|
107
125
|
return {
|
|
108
|
-
update,
|
|
126
|
+
update: (model, message) => updateWith({ isCloseHeld: isCloseHeld(model) })(model, message),
|
|
127
|
+
updateWith,
|
|
109
128
|
open: Update.foldChildStep({ update: child.open, ...lift, foldOutMessage }),
|
|
110
129
|
close: Update.foldChildStep({ update: child.close, ...lift, foldOutMessage }),
|
|
111
130
|
};
|
package/src/dialog/index.ts
CHANGED
|
@@ -6,13 +6,11 @@ export {
|
|
|
6
6
|
close,
|
|
7
7
|
id,
|
|
8
8
|
init,
|
|
9
|
-
isCloseHeld,
|
|
10
9
|
isOpen,
|
|
11
10
|
open,
|
|
12
11
|
sceneMounts,
|
|
13
|
-
setCloseHeld,
|
|
14
12
|
update,
|
|
15
13
|
view,
|
|
16
14
|
} from "./dialog.ts";
|
|
17
|
-
export type { InitConfig, Placement, RenderInfo, ViewInputs } from "./dialog.ts";
|
|
15
|
+
export type { InitConfig, Placement, RenderInfo, UpdateContext, ViewInputs } from "./dialog.ts";
|
|
18
16
|
export { foldDialog } from "./fold.ts";
|
package/src/panel/panel.ts
CHANGED
|
@@ -14,6 +14,9 @@ export type ViewConfig = Readonly<{
|
|
|
14
14
|
id: string;
|
|
15
15
|
/** Pixel caps in the inverted title bar. */
|
|
16
16
|
title: string;
|
|
17
|
+
/** A quiet IconButton before the title, such as the grip a Sortable of panels hands each
|
|
18
|
+
* panel. On the ink bar it prints as `actions` do. */
|
|
19
|
+
lead?: ReadonlyArray<Html | string>;
|
|
17
20
|
/** A count or state at the title bar's right. */
|
|
18
21
|
meta?: string;
|
|
19
22
|
/** Small Buttons and IconButtons at the title bar's right end, after the meta. On the ink
|
|
@@ -32,7 +35,7 @@ export const titleId = (id: string): string => `${id}-title`;
|
|
|
32
35
|
/** The unit of the system: paper in a 2px ink box on a 5px print shadow, under a title bar. A
|
|
33
36
|
* region landmark (`<section>`) named by its `h2` title, keyed by its `id`. */
|
|
34
37
|
export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html => {
|
|
35
|
-
const { id, title, meta, actions = [], body, foot = [], isFlush = false } = config;
|
|
38
|
+
const { id, title, lead = [], meta, actions = [], body, foot = [], isFlush = false } = config;
|
|
36
39
|
|
|
37
40
|
return h.keyed("section")(
|
|
38
41
|
id,
|
|
@@ -41,6 +44,7 @@ export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html
|
|
|
41
44
|
h.header(
|
|
42
45
|
[h.Class("panel-tb")],
|
|
43
46
|
[
|
|
47
|
+
...slotView("span", "panel-lead", lead, h),
|
|
44
48
|
h.h2([h.Id(titleId(id))], [title]),
|
|
45
49
|
...(meta === undefined ? [] : [h.span([h.Class("meta")], [meta])]),
|
|
46
50
|
...slotView("span", "panel-acts", actions, h),
|
package/src/pixel-paper.css
CHANGED
|
@@ -983,23 +983,32 @@ a.textbtn {
|
|
|
983
983
|
flex: 1 1 100%;
|
|
984
984
|
}
|
|
985
985
|
|
|
986
|
-
/* ---------- the panel's title-bar actions ---------- */
|
|
986
|
+
/* ---------- the panel's title-bar lead and actions ---------- */
|
|
987
987
|
|
|
988
|
-
/* small buttons and icon buttons at the bar's right end
|
|
989
|
-
them, as a window's does, so a small button keeps
|
|
990
|
-
|
|
988
|
+
/* small buttons and icon buttons at the bar's right end, and an icon button before the
|
|
989
|
+
title; the bar trims its padding around them, as a window's does, so a small button keeps
|
|
990
|
+
the bar near its one-line height */
|
|
991
|
+
.panel-tb:has(> :is(.panel-lead, .panel-acts)) {
|
|
991
992
|
align-items: center;
|
|
992
993
|
padding-block: 3px;
|
|
993
|
-
padding-right: 3px;
|
|
994
994
|
min-height: 31px;
|
|
995
995
|
}
|
|
996
996
|
|
|
997
|
-
.panel-tb:has(> .panel-
|
|
997
|
+
.panel-tb:has(> .panel-lead) {
|
|
998
|
+
padding-left: 3px;
|
|
999
|
+
}
|
|
1000
|
+
|
|
1001
|
+
.panel-tb:has(> .panel-acts) {
|
|
1002
|
+
padding-right: 3px;
|
|
1003
|
+
}
|
|
1004
|
+
|
|
1005
|
+
.panel-tb:has(> :is(.panel-lead, .panel-acts)) h2 {
|
|
998
1006
|
flex: 1;
|
|
999
1007
|
min-width: 0;
|
|
1000
1008
|
overflow-wrap: anywhere;
|
|
1001
1009
|
}
|
|
1002
1010
|
|
|
1011
|
+
.panel-lead,
|
|
1003
1012
|
.panel-acts {
|
|
1004
1013
|
display: inline-flex;
|
|
1005
1014
|
align-items: center;
|
|
@@ -1014,6 +1023,7 @@ a.textbtn {
|
|
|
1014
1023
|
|
|
1015
1024
|
/* on the ink bar a button prints in paper: an outline, or filled when primary. No print
|
|
1016
1025
|
shadow (it cannot show on ink), so a press nudges 1px, as a toolbar tool does */
|
|
1026
|
+
.panel-lead .btn,
|
|
1017
1027
|
.panel-acts .btn {
|
|
1018
1028
|
color: var(--paper);
|
|
1019
1029
|
background: var(--ink);
|
|
@@ -1026,6 +1036,7 @@ a.textbtn {
|
|
|
1026
1036
|
background: var(--paper);
|
|
1027
1037
|
}
|
|
1028
1038
|
|
|
1039
|
+
.panel-lead .btn:not([aria-disabled="true"]):active,
|
|
1029
1040
|
.panel-acts .btn:not([aria-disabled="true"]):active {
|
|
1030
1041
|
transform: translate(1px, 1px);
|
|
1031
1042
|
box-shadow: none;
|
|
@@ -1038,12 +1049,17 @@ a.textbtn {
|
|
|
1038
1049
|
opacity: 0.6;
|
|
1039
1050
|
}
|
|
1040
1051
|
|
|
1041
|
-
|
|
1052
|
+
/* a quiet icon button prints in paper, and a hover cannot darken it to the ink of the bar */
|
|
1053
|
+
.panel-lead .iconbtn.quiet,
|
|
1054
|
+
.panel-acts .iconbtn.quiet,
|
|
1055
|
+
.panel-lead .iconbtn.quiet:hover,
|
|
1056
|
+
.panel-acts .iconbtn.quiet:hover {
|
|
1042
1057
|
color: inherit;
|
|
1043
1058
|
}
|
|
1044
1059
|
|
|
1045
1060
|
/* the ring sits 4px inside the button in its own text colour, so it reads on an outline and
|
|
1046
1061
|
on a filled one alike */
|
|
1062
|
+
.panel-lead .btn:focus-visible,
|
|
1047
1063
|
.panel-acts .btn:focus-visible {
|
|
1048
1064
|
outline-color: currentColor;
|
|
1049
1065
|
outline-offset: -4px;
|
|
@@ -2692,8 +2708,8 @@ dialog.veil:focus {
|
|
|
2692
2708
|
opacity: 0.75;
|
|
2693
2709
|
}
|
|
2694
2710
|
|
|
2695
|
-
/* a held
|
|
2696
|
-
.card-tb .iconbtn[aria-disabled="true"] {
|
|
2711
|
+
/* a held icon button fades on the bar, as a held button in a panel's title bar does */
|
|
2712
|
+
:is(.card-tb, .panel-tb) .iconbtn[aria-disabled="true"] {
|
|
2697
2713
|
opacity: 0.6;
|
|
2698
2714
|
cursor: default;
|
|
2699
2715
|
}
|
|
@@ -3394,7 +3410,7 @@ dialog.veil:focus {
|
|
|
3394
3410
|
bottom: -1px;
|
|
3395
3411
|
}
|
|
3396
3412
|
|
|
3397
|
-
.sortable.grid .sortable-item:is(.drop-before, .drop-after)::before {
|
|
3413
|
+
.sortable:is(.grid, .panels) .sortable-item:is(.drop-before, .drop-after)::before {
|
|
3398
3414
|
top: -2px;
|
|
3399
3415
|
bottom: -2px;
|
|
3400
3416
|
left: auto;
|
|
@@ -3454,6 +3470,67 @@ dialog.veil:focus {
|
|
|
3454
3470
|
padding: 0;
|
|
3455
3471
|
}
|
|
3456
3472
|
|
|
3473
|
+
/* panels: the parent's own panels, up to three across in the deck's gutter and 300px and up,
|
|
3474
|
+
so two across and then one as the column narrows. A row takes its tallest panel's height,
|
|
3475
|
+
and each panel's foot sits at its bottom */
|
|
3476
|
+
.sortable.panels .sortable-items {
|
|
3477
|
+
display: grid;
|
|
3478
|
+
grid-template-columns: repeat(
|
|
3479
|
+
auto-fill,
|
|
3480
|
+
minmax(min(100%, max(300px, (100% - 2 * var(--gap)) / 3)), 1fr)
|
|
3481
|
+
);
|
|
3482
|
+
gap: var(--gap);
|
|
3483
|
+
}
|
|
3484
|
+
|
|
3485
|
+
.sortable.panels .sortable-item {
|
|
3486
|
+
display: block;
|
|
3487
|
+
padding: 0;
|
|
3488
|
+
border-bottom: 0;
|
|
3489
|
+
background: none;
|
|
3490
|
+
}
|
|
3491
|
+
|
|
3492
|
+
.sortable.panels .sortable-item.movable:hover {
|
|
3493
|
+
background: none;
|
|
3494
|
+
}
|
|
3495
|
+
|
|
3496
|
+
.sortable.panels .sortable-body {
|
|
3497
|
+
height: 100%;
|
|
3498
|
+
}
|
|
3499
|
+
|
|
3500
|
+
.sortable.panels .sortable-body > .panel {
|
|
3501
|
+
display: flex;
|
|
3502
|
+
flex-direction: column;
|
|
3503
|
+
height: 100%;
|
|
3504
|
+
}
|
|
3505
|
+
|
|
3506
|
+
.sortable.panels .sortable-body > .panel > .panel-foot {
|
|
3507
|
+
margin-top: auto;
|
|
3508
|
+
}
|
|
3509
|
+
|
|
3510
|
+
/* the drop line runs down the middle of the gutter */
|
|
3511
|
+
.sortable.panels .sortable-item.drop-before::before {
|
|
3512
|
+
left: calc(var(--gap) / -2 - 1px);
|
|
3513
|
+
}
|
|
3514
|
+
|
|
3515
|
+
.sortable.panels .sortable-item.drop-after::before {
|
|
3516
|
+
right: calc(var(--gap) / -2 - 1px);
|
|
3517
|
+
}
|
|
3518
|
+
|
|
3519
|
+
/* the held copy is the panel itself, tinted head and lifted onto the card shadow */
|
|
3520
|
+
.sortable.panels .sortable-item.lifted {
|
|
3521
|
+
border: 0;
|
|
3522
|
+
box-shadow: none;
|
|
3523
|
+
}
|
|
3524
|
+
|
|
3525
|
+
.sortable.panels .lifted .panel {
|
|
3526
|
+
background: var(--head);
|
|
3527
|
+
box-shadow: var(--shadow-card);
|
|
3528
|
+
}
|
|
3529
|
+
|
|
3530
|
+
.sortable.panels .sortable-slot {
|
|
3531
|
+
padding: 0;
|
|
3532
|
+
}
|
|
3533
|
+
|
|
3457
3534
|
/* while a pointer carries an item, the page grabs and selects nothing */
|
|
3458
3535
|
:root[data-pixel-paper-dragging] {
|
|
3459
3536
|
user-select: none;
|
package/src/sortable/sortable.ts
CHANGED
|
@@ -565,14 +565,17 @@ export const SizeLiftedItem = Mount.define("SizeLiftedItem", {
|
|
|
565
565
|
|
|
566
566
|
// VIEW
|
|
567
567
|
|
|
568
|
-
/** How the items lay out: a column of rows,
|
|
569
|
-
export type Layout = "List" | "Grid";
|
|
568
|
+
/** How the items lay out: a column of rows, tiles that wrap, or the parent's own panels. */
|
|
569
|
+
export type Layout = "List" | "Grid" | "Panels";
|
|
570
570
|
|
|
571
571
|
/** What the list hands `toItemView` for each item. */
|
|
572
572
|
export type ItemRender = Readonly<{
|
|
573
573
|
/** This render is the held copy: hidden from screen readers and inert. Leave out ids and
|
|
574
574
|
* anything the page must find once. */
|
|
575
575
|
isLifted: boolean;
|
|
576
|
+
/** The item's grip, for the `"Panels"` layout, where the item places it: in its panel's
|
|
577
|
+
* `lead`. The other layouts print it before the item themselves. */
|
|
578
|
+
grip: Html;
|
|
576
579
|
}>;
|
|
577
580
|
|
|
578
581
|
/** Per-render inputs passed to `view` through `h.submodel`'s `viewInputs`. The `to*` members
|
|
@@ -582,8 +585,12 @@ export type ViewInputs = Readonly<{
|
|
|
582
585
|
/** Names the list for screen readers. */
|
|
583
586
|
label: string;
|
|
584
587
|
/** `"List"` (the default): rows between 1px rules, for a flush panel. `"Grid"`: tiles that
|
|
585
|
-
* wrap, 180px and up.
|
|
588
|
+
* wrap, 180px and up. `"Panels"`: each item is a Panel the parent prints, with `render.grip`
|
|
589
|
+
* in its `lead`, up to three across in the deck's gutter, 300px and up. */
|
|
586
590
|
layout?: Layout;
|
|
591
|
+
/** The reader may look but not reorder, as a read-only sign-in: every grip prints held, as
|
|
592
|
+
* while moves are held, and nothing lifts. */
|
|
593
|
+
isReadOnly?: boolean;
|
|
587
594
|
/** One item's content, beside its grip. Also called for the held copy, with
|
|
588
595
|
* `render.isLifted`. */
|
|
589
596
|
toItemView: (item: Item, render: ItemRender) => ReadonlyArray<Html>;
|
|
@@ -610,6 +617,8 @@ const isLiftingPress = (target: EventTarget | null, pointerType: string): boolea
|
|
|
610
617
|
|
|
611
618
|
type ListInfo = Readonly<{
|
|
612
619
|
model: Model;
|
|
620
|
+
layout: Layout;
|
|
621
|
+
isMovable: boolean;
|
|
613
622
|
items: ReadonlyArray<Item>;
|
|
614
623
|
maybeDropMark: Option.Option<DropMark>;
|
|
615
624
|
toItemView: (item: Item, render: ItemRender) => ReadonlyArray<Html>;
|
|
@@ -624,6 +633,18 @@ const isHeldItem = (model: Model, itemId: string): boolean =>
|
|
|
624
633
|
const liftedGrip = <ParentMessage>(h: HtmlBuilder<ParentMessage>): Html =>
|
|
625
634
|
h.span([h.Class("iconbtn quiet"), h.AriaHidden(true)], [Glyph.view({ name: "grip" }, h)]);
|
|
626
635
|
|
|
636
|
+
/** The item's content beside its grip, or, in a layout where the item places its grip,
|
|
637
|
+
* alone. */
|
|
638
|
+
const itemContents = <ParentMessage>(
|
|
639
|
+
info: ListInfo,
|
|
640
|
+
item: Item,
|
|
641
|
+
render: ItemRender,
|
|
642
|
+
h: HtmlBuilder<ParentMessage>,
|
|
643
|
+
): ReadonlyArray<Html> => {
|
|
644
|
+
const body = h.div([h.Class("sortable-body")], info.toItemView(item, render));
|
|
645
|
+
return info.layout === "Panels" ? [body] : [render.grip, body];
|
|
646
|
+
};
|
|
647
|
+
|
|
627
648
|
const liftedView = <ParentMessage>(
|
|
628
649
|
info: ListInfo,
|
|
629
650
|
item: Item,
|
|
@@ -631,11 +652,11 @@ const liftedView = <ParentMessage>(
|
|
|
631
652
|
): Html =>
|
|
632
653
|
h.div(
|
|
633
654
|
[h.Class("sortable-item lifted")],
|
|
634
|
-
|
|
655
|
+
itemContents(info, item, { isLifted: true, grip: liftedGrip(h) }, h),
|
|
635
656
|
);
|
|
636
657
|
|
|
637
658
|
const gripView = (info: ListInfo, item: Item, h: HtmlBuilder<Message>): Html => {
|
|
638
|
-
const { model, items } = info;
|
|
659
|
+
const { model, isMovable, items } = info;
|
|
639
660
|
const isIdleNow = phaseOf(model.dragAndDrop) === "Idle";
|
|
640
661
|
|
|
641
662
|
const onGripKey: Attribute<Message> = h.OnKeyDownPreventDefault((key) =>
|
|
@@ -654,14 +675,14 @@ const gripView = (info: ListInfo, item: Item, h: HtmlBuilder<Message>): Html =>
|
|
|
654
675
|
glyph: "grip",
|
|
655
676
|
label: fill(COPY.gripLabel, item.label),
|
|
656
677
|
isQuiet: true,
|
|
657
|
-
isDisabled: !
|
|
678
|
+
isDisabled: !isMovable,
|
|
658
679
|
attributes: [
|
|
659
680
|
h.DataAttribute("draggable-id", itemHook(model, item.id)),
|
|
660
681
|
h.DataAttribute(GRIP_ATTRIBUTE, ""),
|
|
661
682
|
h.AriaRoleDescription("draggable"),
|
|
662
683
|
h.AriaDescribedBy(helpId(model)),
|
|
663
684
|
Button.pressedAttribute(isHeldItem(model, item.id), h),
|
|
664
|
-
...(
|
|
685
|
+
...(isMovable ? [onGripKey] : []),
|
|
665
686
|
],
|
|
666
687
|
},
|
|
667
688
|
h,
|
|
@@ -673,7 +694,7 @@ const itemAttributes = (
|
|
|
673
694
|
item: Item,
|
|
674
695
|
h: HtmlBuilder<Message>,
|
|
675
696
|
): ReadonlyArray<Attribute<Message>> => {
|
|
676
|
-
const { model, items } = info;
|
|
697
|
+
const { model, isMovable, items } = info;
|
|
677
698
|
const isIdleNow = phaseOf(model.dragAndDrop) === "Idle";
|
|
678
699
|
|
|
679
700
|
const onPress = h.OnPointerDown(
|
|
@@ -689,10 +710,7 @@ const itemAttributes = (
|
|
|
689
710
|
items,
|
|
690
711
|
}),
|
|
691
712
|
() =>
|
|
692
|
-
isIdleNow &&
|
|
693
|
-
model.isMovable &&
|
|
694
|
-
button === LEFT_BUTTON &&
|
|
695
|
-
isLiftingPress(target, pointerType),
|
|
713
|
+
isIdleNow && isMovable && button === LEFT_BUTTON && isLiftingPress(target, pointerType),
|
|
696
714
|
),
|
|
697
715
|
);
|
|
698
716
|
|
|
@@ -739,7 +757,7 @@ const dropClass = (info: ListInfo, item: Item): string =>
|
|
|
739
757
|
);
|
|
740
758
|
|
|
741
759
|
const itemClass = (info: ListInfo, item: Item): string =>
|
|
742
|
-
`sortable-item${info.
|
|
760
|
+
`sortable-item${info.isMovable ? " movable" : ""}${isHeldItem(info.model, item.id) ? " drag-dim" : ""}${dropClass(info, item)}`;
|
|
743
761
|
|
|
744
762
|
const itemView = (info: ListInfo, item: Item, h: HtmlBuilder<Message>): Html =>
|
|
745
763
|
h.keyed("li")(
|
|
@@ -749,10 +767,7 @@ const itemView = (info: ListInfo, item: Item, h: HtmlBuilder<Message>): Html =>
|
|
|
749
767
|
...itemAttributes(info, item, h),
|
|
750
768
|
h.Class(itemClass(info, item)),
|
|
751
769
|
],
|
|
752
|
-
|
|
753
|
-
gripView(info, item, h),
|
|
754
|
-
h.div([h.Class("sortable-body")], info.toItemView(item, { isLifted: false })),
|
|
755
|
-
],
|
|
770
|
+
itemContents(info, item, { isLifted: false, grip: gripView(info, item, h) }, h),
|
|
756
771
|
);
|
|
757
772
|
|
|
758
773
|
/** The items in order, with the held item's copy where the keyboard would drop it: before the
|
|
@@ -819,10 +834,20 @@ const liftedCopyView = (info: ListInfo, h: HtmlBuilder<Message>): ReadonlyArray<
|
|
|
819
834
|
);
|
|
820
835
|
};
|
|
821
836
|
|
|
837
|
+
const layoutClass = (layout: Layout): string =>
|
|
838
|
+
Match.value(layout).pipe(
|
|
839
|
+
Match.withReturnType<string>(),
|
|
840
|
+
Match.when("List", () => "sortable"),
|
|
841
|
+
Match.when("Grid", () => "sortable grid"),
|
|
842
|
+
Match.when("Panels", () => "sortable panels"),
|
|
843
|
+
Match.exhaustive,
|
|
844
|
+
);
|
|
845
|
+
|
|
822
846
|
/**
|
|
823
|
-
* Items the reader puts in order, as rows or as
|
|
824
|
-
* `div.sortable` holding `ul.sortable-items` (named by `label`) of
|
|
825
|
-
* grip and the parent's content
|
|
847
|
+
* Items the reader puts in order, as rows, as tiles or as panels, built on `@foldkit/ui`'s
|
|
848
|
+
* DragAndDrop: `div.sortable` holding `ul.sortable-items` (named by `label`) of
|
|
849
|
+
* `li.sortable-item`, each its grip and the parent's content (a panel holds its grip in its
|
|
850
|
+
* title bar). The items are the parent's; a drop reports `Moved`.
|
|
826
851
|
*
|
|
827
852
|
* A mouse drags an item by its body or its grip; touch takes the grip alone, so a finger on
|
|
828
853
|
* an item scrolls the page; the keyboard takes the grip too: Space or Enter picks the item
|
|
@@ -833,11 +858,18 @@ const liftedCopyView = (info: ListInfo, h: HtmlBuilder<Message>): ReadonlyArray<
|
|
|
833
858
|
* the keys.
|
|
834
859
|
*/
|
|
835
860
|
export const view = defineView<Model, Message, ViewInputs>((model, viewInputs, h): Html => {
|
|
836
|
-
const { items, label, layout = "List", toItemView } = viewInputs;
|
|
837
|
-
const info: ListInfo = {
|
|
861
|
+
const { items, label, layout = "List", isReadOnly = false, toItemView } = viewInputs;
|
|
862
|
+
const info: ListInfo = {
|
|
863
|
+
model,
|
|
864
|
+
layout,
|
|
865
|
+
isMovable: model.isMovable && !isReadOnly,
|
|
866
|
+
items,
|
|
867
|
+
maybeDropMark: dropMarkOf(model),
|
|
868
|
+
toItemView,
|
|
869
|
+
};
|
|
838
870
|
|
|
839
871
|
return h.div(
|
|
840
|
-
[h.Class(layout
|
|
872
|
+
[h.Class(layoutClass(layout))],
|
|
841
873
|
[
|
|
842
874
|
// NOTE: Safari takes list semantics from a list printed without markers, so the list
|
|
843
875
|
// says it is one
|