@gjermundgaraba-designs/pixel-paper 0.22.2 → 0.24.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.
Files changed (176) hide show
  1. package/DESIGN.md +18 -17
  2. package/dist/src/adapters/mermaid.d.ts.map +1 -1
  3. package/dist/src/adapters/mermaid.js +0 -24
  4. package/dist/src/announcer/announcer.d.ts.map +1 -1
  5. package/dist/src/announcer/announcer.js +12 -12
  6. package/dist/src/board/board.d.ts +0 -10
  7. package/dist/src/board/board.d.ts.map +1 -1
  8. package/dist/src/board/board.js +36 -64
  9. package/dist/src/board/index.d.ts +2 -1
  10. package/dist/src/board/index.d.ts.map +1 -1
  11. package/dist/src/board/index.js +2 -1
  12. package/dist/src/button/button.d.ts +1 -1
  13. package/dist/src/button/button.d.ts.map +1 -1
  14. package/dist/src/commandPalette/commandPalette.d.ts.map +1 -1
  15. package/dist/src/commandPalette/commandPalette.js +78 -83
  16. package/dist/src/commandPalette/keys.d.ts +2 -2
  17. package/dist/src/commandPalette/keys.d.ts.map +1 -1
  18. package/dist/src/commandPalette/keys.js +2 -4
  19. package/dist/src/commandPalette/match.d.ts.map +1 -1
  20. package/dist/src/commandPalette/match.js +2 -2
  21. package/dist/src/confirmDialog/confirmDialog.d.ts.map +1 -1
  22. package/dist/src/confirmDialog/confirmDialog.js +1 -2
  23. package/dist/src/copyButton/copyButton.d.ts +0 -5
  24. package/dist/src/copyButton/copyButton.d.ts.map +1 -1
  25. package/dist/src/copyButton/copyButton.js +2 -2
  26. package/dist/src/crumbs/crumbs.d.ts +0 -2
  27. package/dist/src/crumbs/crumbs.d.ts.map +1 -1
  28. package/dist/src/crumbs/crumbs.js +2 -2
  29. package/dist/src/dialog/dialog.d.ts +2 -0
  30. package/dist/src/dialog/dialog.d.ts.map +1 -1
  31. package/dist/src/dialog/dialog.js +3 -2
  32. package/dist/src/drag/drag.d.ts +28 -0
  33. package/dist/src/drag/drag.d.ts.map +1 -1
  34. package/dist/src/drag/drag.js +39 -17
  35. package/dist/src/editableTitle/editableTitle.d.ts +4 -4
  36. package/dist/src/field/field.d.ts +7 -1
  37. package/dist/src/field/field.d.ts.map +1 -1
  38. package/dist/src/field/field.js +14 -6
  39. package/dist/src/fieldset/fieldset.d.ts.map +1 -1
  40. package/dist/src/fieldset/fieldset.js +2 -4
  41. package/dist/src/fileTree/fileTree.d.ts.map +1 -1
  42. package/dist/src/fileTree/fileTree.js +16 -16
  43. package/dist/src/iconButton/iconButton.d.ts +6 -1
  44. package/dist/src/iconButton/iconButton.d.ts.map +1 -1
  45. package/dist/src/iconButton/iconButton.js +7 -2
  46. package/dist/src/inlineInput/inlineInput.d.ts +4 -0
  47. package/dist/src/inlineInput/inlineInput.d.ts.map +1 -1
  48. package/dist/src/inlineInput/inlineInput.js +9 -6
  49. package/dist/src/keyHelp/index.d.ts +1 -1
  50. package/dist/src/keyHelp/index.d.ts.map +1 -1
  51. package/dist/src/keyHelp/index.js +1 -1
  52. package/dist/src/keyHelp/keyHelp.d.ts +1 -3
  53. package/dist/src/keyHelp/keyHelp.d.ts.map +1 -1
  54. package/dist/src/keyHelp/keyHelp.js +3 -6
  55. package/dist/src/keyHint/keyLabel.d.ts +2 -0
  56. package/dist/src/keyHint/keyLabel.d.ts.map +1 -1
  57. package/dist/src/keyHint/keyLabel.js +3 -2
  58. package/dist/src/lineDiff/diff.d.ts +6 -3
  59. package/dist/src/lineDiff/diff.d.ts.map +1 -1
  60. package/dist/src/lineDiff/diff.js +40 -28
  61. package/dist/src/lineDiff/patch.d.ts.map +1 -1
  62. package/dist/src/lineDiff/patch.js +18 -13
  63. package/dist/src/linkState/linkState.d.ts.map +1 -1
  64. package/dist/src/linkState/linkState.js +1 -7
  65. package/dist/src/menu/slip.js +2 -2
  66. package/dist/src/meter/meter.d.ts.map +1 -1
  67. package/dist/src/meter/meter.js +22 -17
  68. package/dist/src/notice/notice.d.ts +2 -0
  69. package/dist/src/notice/notice.d.ts.map +1 -1
  70. package/dist/src/notice/notice.js +2 -1
  71. package/dist/src/panel/panel.d.ts +3 -0
  72. package/dist/src/panel/panel.d.ts.map +1 -1
  73. package/dist/src/panel/panel.js +2 -1
  74. package/dist/src/passwordField/passwordField.d.ts.map +1 -1
  75. package/dist/src/passwordField/passwordField.js +3 -7
  76. package/dist/src/rail/index.d.ts +1 -1
  77. package/dist/src/rail/index.d.ts.map +1 -1
  78. package/dist/src/rail/index.js +1 -1
  79. package/dist/src/rail/rail.d.ts +95 -20
  80. package/dist/src/rail/rail.d.ts.map +1 -1
  81. package/dist/src/rail/rail.js +103 -46
  82. package/dist/src/reel/layout.d.ts +9 -5
  83. package/dist/src/reel/layout.d.ts.map +1 -1
  84. package/dist/src/reel/layout.js +11 -5
  85. package/dist/src/reel/reel.d.ts.map +1 -1
  86. package/dist/src/reel/reel.js +122 -68
  87. package/dist/src/scheme/scheme.d.ts +1 -1
  88. package/dist/src/scheme/scheme.d.ts.map +1 -1
  89. package/dist/src/segmented/segmented.d.ts +0 -3
  90. package/dist/src/segmented/segmented.d.ts.map +1 -1
  91. package/dist/src/segmented/segmented.js +1 -6
  92. package/dist/src/selectField/selectField.d.ts.map +1 -1
  93. package/dist/src/selectField/selectField.js +3 -10
  94. package/dist/src/sortable/index.d.ts +1 -1
  95. package/dist/src/sortable/index.d.ts.map +1 -1
  96. package/dist/src/sortable/index.js +1 -1
  97. package/dist/src/sortable/sortable.d.ts +39 -40
  98. package/dist/src/sortable/sortable.d.ts.map +1 -1
  99. package/dist/src/sortable/sortable.js +14 -31
  100. package/dist/src/stack/stack.d.ts +1 -1
  101. package/dist/src/stack/stack.d.ts.map +1 -1
  102. package/dist/src/stack/stack.js +1 -1
  103. package/dist/src/tabs/tabs.d.ts +1 -1
  104. package/dist/src/tabs/tabs.d.ts.map +1 -1
  105. package/dist/src/tabs/tabs.js +1 -1
  106. package/dist/src/textareaField/textareaField.d.ts.map +1 -1
  107. package/dist/src/textareaField/textareaField.js +3 -7
  108. package/dist/src/time/time.d.ts.map +1 -1
  109. package/dist/src/time/time.js +4 -2
  110. package/dist/src/tips/index.d.ts +1 -1
  111. package/dist/src/tips/index.d.ts.map +1 -1
  112. package/dist/src/tips/tips.d.ts +30 -1
  113. package/dist/src/tips/tips.d.ts.map +1 -1
  114. package/dist/src/tips/tips.js +53 -31
  115. package/dist/src/toast/index.d.ts +1 -1
  116. package/dist/src/toast/index.d.ts.map +1 -1
  117. package/dist/src/toast/toast.d.ts +5 -5
  118. package/dist/src/toast/toast.d.ts.map +1 -1
  119. package/dist/src/toast/toast.js +1 -6
  120. package/dist/src/toolbar/toolbar.d.ts +0 -3
  121. package/dist/src/toolbar/toolbar.d.ts.map +1 -1
  122. package/dist/src/toolbar/toolbar.js +1 -2
  123. package/package.json +1 -1
  124. package/src/adapters/mermaid.ts +0 -24
  125. package/src/announcer/announcer.ts +13 -17
  126. package/src/board/board.ts +32 -80
  127. package/src/board/index.ts +1 -1
  128. package/src/button/button.ts +1 -1
  129. package/src/commandPalette/commandPalette.ts +21 -38
  130. package/src/commandPalette/keys.ts +2 -4
  131. package/src/commandPalette/match.ts +3 -3
  132. package/src/confirmDialog/confirmDialog.ts +5 -3
  133. package/src/copyButton/copyButton.ts +2 -5
  134. package/src/crumbs/crumbs.ts +2 -4
  135. package/src/dialog/dialog.ts +3 -2
  136. package/src/drag/drag.ts +54 -5
  137. package/src/field/field.ts +29 -7
  138. package/src/fieldset/fieldset.ts +2 -4
  139. package/src/fileTree/fileTree.ts +10 -12
  140. package/src/iconButton/iconButton.ts +19 -2
  141. package/src/inlineInput/inlineInput.ts +11 -2
  142. package/src/keyHelp/index.ts +0 -1
  143. package/src/keyHelp/keyHelp.ts +3 -9
  144. package/src/keyHint/keyLabel.ts +5 -2
  145. package/src/lineDiff/diff.ts +70 -54
  146. package/src/lineDiff/patch.ts +27 -14
  147. package/src/linkState/linkState.ts +1 -8
  148. package/src/menu/slip.ts +2 -2
  149. package/src/meter/meter.ts +25 -15
  150. package/src/notice/notice.ts +2 -1
  151. package/src/panel/panel.ts +5 -0
  152. package/src/passwordField/passwordField.ts +3 -7
  153. package/src/pixel-paper.css +200 -134
  154. package/src/rail/index.ts +2 -0
  155. package/src/rail/rail.ts +124 -44
  156. package/src/reel/layout.ts +14 -7
  157. package/src/reel/reel.ts +132 -63
  158. package/src/scheme/scheme.ts +1 -1
  159. package/src/segmented/segmented.ts +1 -9
  160. package/src/selectField/selectField.ts +3 -10
  161. package/src/sortable/index.ts +0 -1
  162. package/src/sortable/sortable.ts +35 -54
  163. package/src/stack/stack.ts +1 -1
  164. package/src/tabs/tabs.ts +1 -1
  165. package/src/textareaField/textareaField.ts +3 -7
  166. package/src/time/time.ts +6 -7
  167. package/src/tips/index.ts +1 -1
  168. package/src/tips/tips.ts +71 -35
  169. package/src/toast/index.ts +1 -1
  170. package/src/toast/toast.ts +2 -8
  171. package/src/tokens.css +7 -0
  172. package/src/toolbar/toolbar.ts +0 -5
  173. package/dist/src/inlineInput/inline.d.ts +0 -6
  174. package/dist/src/inlineInput/inline.d.ts.map +0 -1
  175. package/dist/src/inlineInput/inline.js +0 -7
  176. package/src/inlineInput/inline.ts +0 -13
package/src/reel/reel.ts CHANGED
@@ -1,4 +1,16 @@
1
- import { Array, Effect, Equal, Match, Number, Option, Order, Schema, Stream, String } from "effect";
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) => !Equal.equals(apply(layout, request), layout)),
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, and the middles of its slots
827
- * but the held pane's. */
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 their slots sit: none outside the reel. */
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
- targetAtPoint(
867
- Array.map(Array.fromIterable(view.querySelectorAll("[data-reel-column]")), (element) =>
868
- columnBoxOf(element, paneId),
869
- ),
870
- clientX,
871
- clientY,
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
- dragMessages<Message>(hold.pointerId, {
891
- moved: (event) => Message.MovedPointer(pointerAt(hold, event)),
892
- released: (event) => Message.ReleasedPointer(pointerAt(hold, event)),
893
- cancelled: () => Message.CancelledPointer(),
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
- const dropClass = (info: ReelInfo, column: Column, paneId: string): string =>
1228
- Option.match(
1229
- Option.flatMap(info.maybeMark, (mark) => {
1230
- if (mark._tag !== "Into" || mark.columnId !== column.id) {
1231
- return Option.none();
1232
- }
1233
- const others = Array.filter(
1234
- column.panes,
1235
- (pane) => !Option.contains(info.maybeCarriedId, pane.id),
1236
- );
1237
- return Option.orElse(
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.get(others, mark.index), (pane) => pane.id === paneId),
1240
- () => " drop-before",
1313
+ Option.filter(Array.last(others), () => mark.index >= others.length),
1314
+ ({ id }) => ({ paneId: id, markClass: " drop-after" }),
1241
1315
  ),
1242
- () =>
1243
- Option.map(
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
- column: Column,
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(`reel-slot${dropClass(info, column, pane.id)}`),
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(info, anchors, column, pane, h),
1371
+ slotView(anchors, maybeDropMark, pane, h),
1303
1372
  ]);
1304
1373
  const maybeNextId = Option.map(Array.get(layout, index + 1), ({ id }) => id);
1305
1374
 
@@ -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 | undefined): Scheme =>
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 { describedByError, errorView } from "../field/field.ts";
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 === undefined
92
- ? []
93
- : [h.p([...groups.description, h.Class("field-help")], [help])]),
86
+ ...helpView(help, groups.description, h),
94
87
  ],
95
88
  ),
96
89
  },
@@ -6,7 +6,6 @@ export {
6
6
  OutMessage,
7
7
  SizeLiftedItem,
8
8
  init,
9
- isHolding,
10
9
  setMovable,
11
10
  subscriptions,
12
11
  update,
@@ -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
- escapeKeys,
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
- const nextModel =
288
- phaseOf(dragAndDropUpdate.model.dragAndDrop) === "Idle"
289
- ? modifyFields(dragAndDropUpdate.model, { maybeHeld: () => Option.none() })
290
- : dragAndDropUpdate.model;
291
- const maybeText = Option.flatMap(previous.maybeHeld, (held) =>
292
- moveText(
293
- held,
294
- previous,
295
- dragAndDropUpdate.model,
296
- Option.fromNullishOr(dragAndDropUpdate.outMessage),
297
- putBack,
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.flatMap(indexOf(items, itemId), (index) =>
326
- Option.map(Array.get(items, index), (item) => ({ index, item })),
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 ownSubscriptions = Subscription.make<Model, Message>()((entry) => ({
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(ownSubscriptions, dragSubscriptions);
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
@@ -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). Shared by Stack, Group and Wrap. */
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
 
package/src/tabs/tabs.ts CHANGED
@@ -40,7 +40,7 @@ export type OutMessage = typeof OutMessage.Type;
40
40
  * document; each tab's and panel's id derives from it. */
41
41
  export type InitConfig = Readonly<{ id: string }>;
42
42
 
43
- /** Creates a tab strip. Arrows select as they move, as the strip has always done. */
43
+ /** Creates a tab strip. Arrows select as they move. */
44
44
  export const init = (config: InitConfig): Model => ({
45
45
  tabs: UiTabs.init({ id: config.id, activationMode: "Automatic" }),
46
46
  });
@@ -1,7 +1,7 @@
1
1
  import { Textarea as UiTextarea } from "@foldkit/ui";
2
2
  import type { ChildAttribute, Html, HtmlBuilder, TextareaAttribute } from "foldkit/html";
3
3
 
4
- import { describedByError, errorView } from "../field/field.ts";
4
+ import { controlAttributes, errorView, helpView } from "../field/field.ts";
5
5
 
6
6
  // VIEW
7
7
 
@@ -59,17 +59,13 @@ export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Messag
59
59
  [h.Class(`field${isMono ? " mono" : ""}`)],
60
60
  [
61
61
  h.label(groups.label, [label]),
62
- // NOTE: @foldkit/ui gap: Textarea has no required option, so the wrapper says it.
63
62
  h.textarea([
64
63
  ...textareaAttributes,
65
64
  ...groups.textarea,
66
- ...describedByError(id, error, help !== undefined, h),
67
- ...(isRequired ? [h.AriaRequired(true)] : []),
65
+ ...controlAttributes(id, error, help, isRequired, h),
68
66
  ]),
69
67
  ...errorView(id, error, h),
70
- ...(help === undefined
71
- ? []
72
- : [h.p([...groups.description, h.Class("field-help")], [help])]),
68
+ ...helpView(help, groups.description, h),
73
69
  ],
74
70
  ),
75
71
  },
package/src/time/time.ts CHANGED
@@ -1,4 +1,4 @@
1
- import { Clock, Duration, Effect, Match, Option, Schedule, Schema, Stream } from "effect";
1
+ import { Clock, Duration, Match, Option, Schedule, Schema, Stream } from "effect";
2
2
  import type { Html, HtmlBuilder } from "foldkit/html";
3
3
  import type { EntryWithoutKeepAlive } from "foldkit/subscription";
4
4
 
@@ -127,10 +127,9 @@ export const clockEntry = <Model, Message>(
127
127
  dependenciesSchema: Schema.Struct({ isActive: Schema.Boolean }),
128
128
  modelToDependencies: (model) => ({ isActive: config.isActive(model) }),
129
129
  dependenciesToStream: ({ isActive }) =>
130
- Stream.when(
131
- Stream.fromEffectSchedule(Clock.currentTimeMillis, Schedule.spaced(TICK)).pipe(
132
- Stream.map(config.toMessage),
133
- ),
134
- Effect.sync(() => isActive),
135
- ),
130
+ isActive
131
+ ? Stream.fromEffectSchedule(Clock.currentTimeMillis, Schedule.spaced(TICK)).pipe(
132
+ Stream.map(config.toMessage),
133
+ )
134
+ : Stream.empty,
136
135
  });
package/src/tips/index.ts CHANGED
@@ -12,4 +12,4 @@ export {
12
12
  update,
13
13
  view,
14
14
  } from "./tips.ts";
15
- export type { InitConfig, Trigger } from "./tips.ts";
15
+ export type { InitConfig } from "./tips.ts";