@gjermundgaraba-designs/pixel-paper 0.20.1 → 0.22.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 (74) hide show
  1. package/DESIGN.md +21 -15
  2. package/dist/src/bench/bench.d.ts +14 -0
  3. package/dist/src/bench/bench.d.ts.map +1 -0
  4. package/dist/src/bench/bench.js +9 -0
  5. package/dist/src/bench/index.d.ts +3 -0
  6. package/dist/src/bench/index.d.ts.map +1 -0
  7. package/dist/src/bench/index.js +1 -0
  8. package/dist/src/chip/chip.d.ts +10 -0
  9. package/dist/src/chip/chip.d.ts.map +1 -1
  10. package/dist/src/chip/chip.js +19 -0
  11. package/dist/src/chip/index.d.ts +2 -2
  12. package/dist/src/chip/index.d.ts.map +1 -1
  13. package/dist/src/chip/index.js +1 -1
  14. package/dist/src/drag/drag.d.ts +18 -0
  15. package/dist/src/drag/drag.d.ts.map +1 -1
  16. package/dist/src/drag/drag.js +35 -7
  17. package/dist/src/field/field.d.ts +3 -0
  18. package/dist/src/field/field.d.ts.map +1 -1
  19. package/dist/src/field/field.js +2 -2
  20. package/dist/src/index.d.ts +1 -0
  21. package/dist/src/index.d.ts.map +1 -1
  22. package/dist/src/index.js +1 -0
  23. package/dist/src/lineDiff/index.d.ts +2 -0
  24. package/dist/src/lineDiff/index.d.ts.map +1 -1
  25. package/dist/src/lineDiff/index.js +1 -0
  26. package/dist/src/lineDiff/lineDiff.d.ts +8 -4
  27. package/dist/src/lineDiff/lineDiff.d.ts.map +1 -1
  28. package/dist/src/lineDiff/lineDiff.js +19 -15
  29. package/dist/src/lineDiff/patch.d.ts +35 -0
  30. package/dist/src/lineDiff/patch.d.ts.map +1 -0
  31. package/dist/src/lineDiff/patch.js +146 -0
  32. package/dist/src/menu/menu.d.ts +6 -6
  33. package/dist/src/rail/index.d.ts +1 -1
  34. package/dist/src/rail/index.d.ts.map +1 -1
  35. package/dist/src/rail/index.js +1 -1
  36. package/dist/src/rail/rail.d.ts +217 -17
  37. package/dist/src/rail/rail.d.ts.map +1 -1
  38. package/dist/src/rail/rail.js +133 -15
  39. package/dist/src/reel/index.d.ts +1 -1
  40. package/dist/src/reel/index.d.ts.map +1 -1
  41. package/dist/src/reel/index.js +1 -1
  42. package/dist/src/reel/layout.d.ts +21 -26
  43. package/dist/src/reel/layout.d.ts.map +1 -1
  44. package/dist/src/reel/layout.js +84 -106
  45. package/dist/src/reel/reel.d.ts +30 -172
  46. package/dist/src/reel/reel.d.ts.map +1 -1
  47. package/dist/src/reel/reel.js +144 -195
  48. package/dist/src/rows/rows.d.ts +9 -1
  49. package/dist/src/rows/rows.d.ts.map +1 -1
  50. package/dist/src/rows/rows.js +1 -1
  51. package/dist/src/sortable/sortable.d.ts +30 -30
  52. package/dist/src/toast/toast.d.ts +12 -12
  53. package/dist/src/window/window.d.ts +3 -1
  54. package/dist/src/window/window.d.ts.map +1 -1
  55. package/package.json +1 -1
  56. package/src/bench/bench.ts +24 -0
  57. package/src/bench/index.ts +2 -0
  58. package/src/chip/chip.ts +31 -0
  59. package/src/chip/index.ts +2 -2
  60. package/src/drag/drag.ts +60 -1
  61. package/src/field/field.ts +5 -1
  62. package/src/index.ts +1 -0
  63. package/src/lineDiff/index.ts +2 -0
  64. package/src/lineDiff/lineDiff.ts +29 -24
  65. package/src/lineDiff/patch.ts +218 -0
  66. package/src/pixel-paper.css +202 -32
  67. package/src/rail/index.ts +2 -0
  68. package/src/rail/rail.ts +181 -16
  69. package/src/reel/index.ts +0 -1
  70. package/src/reel/layout.ts +78 -118
  71. package/src/reel/reel.ts +108 -216
  72. package/src/rows/rows.ts +14 -2
  73. package/src/tokens.css +2 -1
  74. package/src/window/window.ts +3 -1
@@ -0,0 +1,218 @@
1
+ import { Array, Option, String, pipe } from "effect";
2
+
3
+ import { type DiffRow, diffRowsOfPatch } from "./diff.ts";
4
+
5
+ // QUOTED PATHS
6
+
7
+ const ESCAPED_BYTES: Readonly<Record<string, number>> = {
8
+ a: 7,
9
+ b: 8,
10
+ t: 9,
11
+ n: 10,
12
+ v: 11,
13
+ f: 12,
14
+ r: 13,
15
+ };
16
+
17
+ // an octal escape is one byte of the name's UTF-8, so the bytes are gathered before decoding
18
+ const QUOTED_PART = /\\([0-7]{3})|\\(.)|[^\\]+/gu;
19
+
20
+ const bytesOf = (text: string): ReadonlyArray<number> => [...new TextEncoder().encode(text)];
21
+
22
+ const bytesOfPart = ([part, octal, escaped]: RegExpExecArray): ReadonlyArray<number> => {
23
+ if (octal !== undefined) {
24
+ return [Number.parseInt(octal, 8)];
25
+ } else if (escaped !== undefined) {
26
+ return Option.match(Option.fromUndefinedOr(ESCAPED_BYTES[escaped]), {
27
+ onNone: () => bytesOf(escaped),
28
+ onSome: (byte) => [byte],
29
+ });
30
+ } else {
31
+ return bytesOf(part);
32
+ }
33
+ };
34
+
35
+ /** The name a path in git's header stands for: git C-quotes a name with unusual bytes
36
+ * (`"caf\303\251.txt"`), and leaves any other as it is. */
37
+ export const unquoted = (path: string): string =>
38
+ path.length >= 2 && path.startsWith('"') && path.endsWith('"')
39
+ ? new TextDecoder().decode(
40
+ Uint8Array.from(
41
+ Array.flatMap(Array.fromIterable(path.slice(1, -1).matchAll(QUOTED_PART)), bytesOfPart),
42
+ ),
43
+ )
44
+ : path;
45
+
46
+ // HEADER
47
+
48
+ const GIT_SECTION = "diff --git ";
49
+
50
+ // the header is what comes before the first hunk, so a changed line that reads like a header
51
+ // line ("+rename from x") is never taken for one
52
+ const headOf = (section: string): ReadonlyArray<string> =>
53
+ Array.takeWhile(
54
+ section.split("\n"),
55
+ (line) => !line.startsWith("@@") && !line.startsWith("--- "),
56
+ );
57
+
58
+ const valueAfter = (head: ReadonlyArray<string>, prefix: string): Option.Option<string> =>
59
+ pipe(
60
+ Array.findFirst(head, String.startsWith(prefix)),
61
+ Option.map((line) => line.slice(prefix.length)),
62
+ );
63
+
64
+ const hasLine = (head: ReadonlyArray<string>, prefix: string): boolean =>
65
+ Option.isSome(valueAfter(head, prefix));
66
+
67
+ const hasHunk = (section: string): boolean =>
68
+ Array.some(section.split("\n"), String.startsWith("@@ "));
69
+
70
+ // git writes one section per file, after whatever precedes the first (a commit's message, a
71
+ // mail's head); a patch without a git header is one section
72
+ const sectionsOf = (patch: string): ReadonlyArray<string> => {
73
+ const [first = "", ...others] = patch.split(/(?=^diff --git )/mu);
74
+
75
+ return Array.isArrayNonEmpty(others) && !first.startsWith(GIT_SECTION)
76
+ ? others
77
+ : [first, ...others];
78
+ };
79
+
80
+ // READING
81
+
82
+ /** What a diff of two same texts says. */
83
+ export const SAME_NOTE = "No difference: both sides hold the same text.";
84
+ const UNREADABLE_NOTE = "This diff cannot be shown as lines.";
85
+ const BINARY_NOTE = "A binary file: its change has no lines.";
86
+
87
+ /** A table of the patch's lines; `maybeSide` says which side of a type change it is. */
88
+ type Table = Readonly<{ rows: ReadonlyArray<DiffRow>; maybeSide: Option.Option<string> }>;
89
+
90
+ /** What `patchView` draws: its notes as a line, then its tables. */
91
+ export type PatchReading = Readonly<{
92
+ maybeRenamedFrom: Option.Option<string>;
93
+ notes: ReadonlyArray<string>;
94
+ tables: ReadonlyArray<Table>;
95
+ }>;
96
+
97
+ const unreadable: PatchReading = {
98
+ maybeRenamedFrom: Option.none(),
99
+ notes: [UNREADABLE_NOTE],
100
+ tables: [],
101
+ };
102
+
103
+ const lineNote = (head: ReadonlyArray<string>, hasRows: boolean): Option.Option<string> => {
104
+ if (hasLine(head, "Binary files ") || hasLine(head, "GIT binary patch")) {
105
+ return Option.some(BINARY_NOTE);
106
+ } else if (hasRows) {
107
+ return Option.none();
108
+ } else if (hasLine(head, "new file mode ")) {
109
+ return Option.some("New empty file.");
110
+ } else if (hasLine(head, "deleted file mode ")) {
111
+ return Option.some("Deleted empty file.");
112
+ } else {
113
+ return Option.none();
114
+ }
115
+ };
116
+
117
+ const readSection = (section: string): PatchReading => {
118
+ const head = headOf(section);
119
+ const maybeRows = diffRowsOfPatch(section);
120
+ const maybeRenamedFrom = Option.map(valueAfter(head, "rename from "), unquoted);
121
+ const notes = [
122
+ ...Option.toArray(Option.map(maybeRenamedFrom, (from) => `Renamed from ${from}.`)),
123
+ ...Option.toArray(
124
+ Option.map(
125
+ Option.all({ before: valueAfter(head, "old mode "), after: valueAfter(head, "new mode ") }),
126
+ ({ before, after }) => `Mode ${before} → ${after}.`,
127
+ ),
128
+ ),
129
+ ...Option.toArray(lineNote(head, Option.isSome(maybeRows))),
130
+ ];
131
+ const isUnread = Option.isNone(maybeRows) && (hasHunk(section) || Array.isArrayEmpty(notes));
132
+
133
+ return isUnread
134
+ ? unreadable
135
+ : {
136
+ maybeRenamedFrom,
137
+ notes,
138
+ tables: Option.toArray(
139
+ Option.map(maybeRows, (rows) => ({ rows, maybeSide: Option.none() })),
140
+ ),
141
+ };
142
+ };
143
+
144
+ const KINDS: Readonly<Record<string, string>> = {
145
+ "100644": "a file",
146
+ "100755": "an executable file",
147
+ "120000": "a symlink",
148
+ "160000": "a submodule",
149
+ };
150
+
151
+ const kindOf = (mode: string): string => KINDS[mode] ?? `mode ${mode}`;
152
+
153
+ const sideTable = (section: string, side: string): ReadonlyArray<Table> =>
154
+ Option.toArray(
155
+ Option.map(diffRowsOfPatch(section), (rows) => ({ rows, maybeSide: Option.some(side) })),
156
+ );
157
+
158
+ // a file that became a symlink (or any other kind) is two sections for one path: the old
159
+ // kind deleted, then the new one added
160
+ const readTypeChange = (gone: string, added: string): Option.Option<PatchReading> => {
161
+ const [goneHead, addedHead] = [headOf(gone), headOf(added)];
162
+
163
+ return Option.flatMap(
164
+ Option.all({
165
+ before: valueAfter(goneHead, "deleted file mode "),
166
+ after: valueAfter(addedHead, "new file mode "),
167
+ }),
168
+ ({ before, after }) =>
169
+ goneHead[0] === addedHead[0]
170
+ ? Option.some({
171
+ maybeRenamedFrom: Option.none(),
172
+ notes: [`It was ${kindOf(before)} and is now ${kindOf(after)}.`],
173
+ tables: [...sideTable(gone, "as it was"), ...sideTable(added, "as it is")],
174
+ })
175
+ : Option.none(),
176
+ );
177
+ };
178
+
179
+ /** What `patchView` makes of a patch: the notes it prints and the tables it draws. */
180
+ export const readPatch = (patch: string): PatchReading => {
181
+ const text = patch.replaceAll("\r\n", "\n");
182
+
183
+ if (String.isEmpty(String.trim(text))) {
184
+ return { maybeRenamedFrom: Option.none(), notes: [SAME_NOTE], tables: [] };
185
+ } else {
186
+ const sections = sectionsOf(text);
187
+ const [first, second, ...rest] = sections;
188
+
189
+ if (first !== undefined && second === undefined) {
190
+ return readSection(first);
191
+ } else if (first !== undefined && second !== undefined && Array.isArrayEmpty(rest)) {
192
+ return Option.getOrElse(readTypeChange(first, second), () => unreadable);
193
+ } else {
194
+ return unreadable;
195
+ }
196
+ }
197
+ };
198
+
199
+ // DESCRIPTION
200
+
201
+ /** What a patch says beyond its lines, as `patchView` prints it. */
202
+ export type PatchDescription = Readonly<{
203
+ /** The path a rename left, unquoted: for a heading such as `old.txt → new.txt`. */
204
+ maybeRenamedFrom: Option.Option<string>;
205
+ /** The sentences `patchView` prints above the patch's lines, or in their place: a rename,
206
+ * a mode change, a type change, an empty or binary file, no difference, or a patch it
207
+ * cannot show as lines. */
208
+ notes: ReadonlyArray<string>;
209
+ }>;
210
+
211
+ /** What `patchView` says about a patch from `git diff`, read from its header: the notes it
212
+ * prints (`["Renamed from old.txt."]`), for a caller that prints them itself, and the path a
213
+ * rename left. */
214
+ export const describePatch = (patch: string): PatchDescription => {
215
+ const { maybeRenamedFrom, notes } = readPatch(patch);
216
+
217
+ return { maybeRenamedFrom, notes };
218
+ };
@@ -1249,6 +1249,43 @@ a.fchip {
1249
1249
  border-color: var(--muted);
1250
1250
  }
1251
1251
 
1252
+ /* a chip as a cell, where its word would take a title's room: a 10px square, hollow where the
1253
+ chip is plain and filled where it has a tone, so plain and toned differ by form, not hue.
1254
+ Blocked fills red, where the chip only outlines it, and done fills muted, where the chip only
1255
+ tints, since neither an outline nor a tint reads in a 6px cell. Its word is its name and its
1256
+ tip. In a row it keeps the 18px of the glyph cell it stands in */
1257
+ .chip-dot {
1258
+ display: inline-block;
1259
+ flex: none;
1260
+ width: 10px;
1261
+ height: 10px;
1262
+ border: var(--line) solid var(--ink);
1263
+ background: transparent;
1264
+ }
1265
+
1266
+ .chip-dot.go {
1267
+ background: var(--green);
1268
+ }
1269
+
1270
+ .chip-dot.warn {
1271
+ background: var(--amber);
1272
+ }
1273
+
1274
+ .chip-dot.blocked {
1275
+ background: var(--red);
1276
+ border-color: var(--red);
1277
+ }
1278
+
1279
+ .chip-dot.done {
1280
+ background: var(--muted);
1281
+ border-color: var(--muted);
1282
+ }
1283
+
1284
+ .row > .chip-dot,
1285
+ .row > .cell > .chip-dot {
1286
+ margin-inline: 4px;
1287
+ }
1288
+
1252
1289
  /* Black on amber preserves 3:1 contrast; an amber glyph on paper does not. */
1253
1290
  .mark {
1254
1291
  display: inline-flex;
@@ -1438,6 +1475,74 @@ button.glyph {
1438
1475
  gap: 6px;
1439
1476
  }
1440
1477
 
1478
+ /* a row whose box is its link's target: the link's ::after covers the row (the row places it;
1479
+ the text's clip does not, being no containing block). Only the row's controls and what has a
1480
+ tip sit above it, so a menu stays its own target and a dot's tip still shows, while a chip or
1481
+ a due day is the link's like the rest of the row */
1482
+ .row.linked .grow a {
1483
+ color: inherit;
1484
+ text-decoration: none;
1485
+ }
1486
+
1487
+ .row.linked .grow a::after {
1488
+ content: "";
1489
+ position: absolute;
1490
+ inset: 0;
1491
+ }
1492
+
1493
+ .row.linked :is(button, input, select, textarea, [data-tip], .row-check) {
1494
+ position: relative;
1495
+ z-index: 1;
1496
+ }
1497
+
1498
+ .row.linked:not(:has(.grow a[aria-current])):hover .grow a {
1499
+ text-decoration: underline;
1500
+ text-underline-offset: 3px;
1501
+ }
1502
+
1503
+ /* the keyboard's one stop is the link; its ring is the row's, 4px inside it */
1504
+ .row.linked .grow a:focus-visible {
1505
+ outline: 0;
1506
+ }
1507
+
1508
+ .row.linked:has(.grow a:focus-visible) {
1509
+ outline: var(--line) solid var(--ink);
1510
+ outline-offset: -4px;
1511
+ }
1512
+
1513
+ /* the row you are on (its link is aria-current) prints inverted, as a place and a board's
1514
+ cursor card do: hover adds nothing, a dot's border takes the paper, what prints muted on
1515
+ paper prints in paper at 0.75, and lateness takes a red slip */
1516
+ .row.linked:has(.grow a[aria-current]) {
1517
+ background-color: var(--ink);
1518
+ color: var(--paper);
1519
+ }
1520
+
1521
+ .row.linked:has(.grow a[aria-current]) .chip-dot {
1522
+ border-color: var(--paper);
1523
+ }
1524
+
1525
+ .row.linked:has(.grow a[aria-current]) :is(.glyph, .age, .due, .tag, .done-title, .iconbtn.quiet) {
1526
+ color: inherit;
1527
+ opacity: 0.75;
1528
+ }
1529
+
1530
+ .row.linked:has(.grow a[aria-current]) .tag {
1531
+ border-color: currentColor;
1532
+ }
1533
+
1534
+ .row.linked:has(.grow a[aria-current]) :is(.due.late, .age.stale) {
1535
+ opacity: 1;
1536
+ background: var(--red);
1537
+ color: var(--on-red);
1538
+ box-shadow: 0 0 0 3px var(--red);
1539
+ }
1540
+
1541
+ .row.linked:has(.grow a[aria-current]) :focus-visible,
1542
+ .row.linked:has(.grow a[aria-current]:focus-visible) {
1543
+ outline-color: var(--paper);
1544
+ }
1545
+
1441
1546
  /* the cursor: the one row (or board card) the keyboard's commands act on. It is not focus
1442
1547
  (the ring, which can be elsewhere) and not selection (the box's cell).
1443
1548
 
@@ -4962,7 +5067,8 @@ dialog.veil:focus {
4962
5067
 
4963
5068
  /* one element in two places: at rest, fixed down the left edge from the top bar's lower
4964
5069
  edge (as the stage hangs) and scrolling itself; on a phone the same element opens as a
4965
- popover drawer */
5070
+ popover drawer. --rail is the reader's width when they gave one: the view sets it on
5071
+ .railed, so the page's padding and the rail's width follow it together */
4966
5072
  .railed {
4967
5073
  padding-left: var(--rail);
4968
5074
  }
@@ -4990,6 +5096,41 @@ dialog.veil:focus {
4990
5096
  min-width: 0;
4991
5097
  }
4992
5098
 
5099
+ /* the splitter on the rail's right edge: a 12px strip from the edge outward, over the page's
5100
+ margin and never over the rail's own scrollbar, and beside the rail rather than in it (the
5101
+ rail scrolls, and its drawer's first Tab stop is its first control). It shows itself as a
5102
+ 2px ink line down the edge while it is hovered or focused */
5103
+ .rail-resize {
5104
+ position: fixed;
5105
+ top: anchor(--topbar bottom, 0px);
5106
+ bottom: var(--statusbar-h, 0px);
5107
+ left: var(--rail);
5108
+ z-index: 1;
5109
+ width: 12px;
5110
+ cursor: col-resize;
5111
+ touch-action: none;
5112
+ }
5113
+
5114
+ .rail-resize::after {
5115
+ content: "";
5116
+ position: absolute;
5117
+ top: 0;
5118
+ bottom: 0;
5119
+ left: 0;
5120
+ width: var(--line);
5121
+ background: var(--ink);
5122
+ opacity: 0;
5123
+ pointer-events: none;
5124
+ }
5125
+
5126
+ .rail-resize:is(:hover, :focus-visible)::after {
5127
+ opacity: 1;
5128
+ }
5129
+
5130
+ .rail-resize:focus-visible {
5131
+ outline-offset: -2px;
5132
+ }
5133
+
4993
5134
  .rail-head,
4994
5135
  .rail-open {
4995
5136
  display: none;
@@ -5000,14 +5141,15 @@ dialog.veil:focus {
5000
5141
  padding-left: 0;
5001
5142
  }
5002
5143
 
5003
- .rail[popover]:not(:popover-open) {
5144
+ .rail[popover]:not(:popover-open),
5145
+ .rail-resize {
5004
5146
  display: none;
5005
5147
  }
5006
5148
 
5007
5149
  /* a slice of the ground slides in from the left edge, in three steps */
5008
5150
  .rail[popover]:popover-open {
5009
5151
  inset: 0 auto 0 0;
5010
- width: min(calc(var(--rail) + 48px), calc(100vw - 40px));
5152
+ width: min(var(--rail-drawer), calc(100vw - 40px));
5011
5153
  height: 100dvh;
5012
5154
  max-height: none;
5013
5155
  padding: 12px 16px var(--gap) 12px;
@@ -5040,6 +5182,49 @@ dialog.veil:focus {
5040
5182
  }
5041
5183
  }
5042
5184
 
5185
+ /* ---------- the bench: a page's work, the window wide, under its head ---------- */
5186
+
5187
+ /* hung from the top bar's lower edge like the stage, down to the status bar, edge to edge, or
5188
+ from the rail's edge beside one (a fixed box takes nothing from .railed's padding). The head
5189
+ takes its own height; the work takes the rest and lays out what fills it (a reel's height
5190
+ is 100% of it). The page does not scroll */
5191
+ .bench {
5192
+ position: fixed;
5193
+ top: anchor(--topbar bottom, 0px);
5194
+ right: 0;
5195
+ bottom: var(--statusbar-h, 0px);
5196
+ left: 0;
5197
+ display: grid;
5198
+ grid-template-rows: auto minmax(0, 1fr);
5199
+ }
5200
+
5201
+ .railed > .bench {
5202
+ left: var(--rail);
5203
+ }
5204
+
5205
+ /* the head lines up with a reel's windows, a reel's gutter in from the edge; the reel's own
5206
+ gutter is the space between the head and the windows */
5207
+ .bench-head {
5208
+ display: grid;
5209
+ gap: var(--gap);
5210
+ padding: var(--gap) var(--reel-gutter) 0;
5211
+ }
5212
+
5213
+ .bench-head > *,
5214
+ .bench-work {
5215
+ min-width: 0;
5216
+ }
5217
+
5218
+ .bench-work {
5219
+ min-height: 0;
5220
+ }
5221
+
5222
+ @media (max-width: 900px) {
5223
+ .railed > .bench {
5224
+ left: 0;
5225
+ }
5226
+ }
5227
+
5043
5228
  /* ---------- the stage: the ground itself, under the top bar, for a canvas ---------- */
5044
5229
 
5045
5230
  /* it does not scroll: what is on it pans. It hangs from the top bar's lower edge */
@@ -5151,6 +5336,12 @@ dialog.veil:focus {
5151
5336
  color: inherit;
5152
5337
  }
5153
5338
 
5339
+ /* a child Submodel's tools come back as one element; the group is no box of its own, so its
5340
+ buttons take the bar's gap as the others do */
5341
+ .window-tools {
5342
+ display: contents;
5343
+ }
5344
+
5154
5345
  .window-body {
5155
5346
  flex: 1;
5156
5347
  min-height: 0;
@@ -5203,7 +5394,7 @@ dialog.veil:focus {
5203
5394
  width: max-content;
5204
5395
  min-width: 100%;
5205
5396
  min-height: 100%;
5206
- padding-bottom: var(--reel-gutter);
5397
+ padding-block: var(--reel-gutter);
5207
5398
  }
5208
5399
 
5209
5400
  .reel-gap {
@@ -5215,7 +5406,8 @@ dialog.veil:focus {
5215
5406
 
5216
5407
  /* a column is width × (view − gutter) − gutter wide, so columns whose widths add up to one
5217
5408
  fill the view with a gutter between each and at either side; never under the minimum. It is
5218
- the view's height, and taller where its panes need more, which the view scrolls to */
5409
+ the view's height less a gutter above and below, and taller where its panes need more, which
5410
+ the view scrolls to */
5219
5411
  .reel-column {
5220
5412
  display: flex;
5221
5413
  flex: none;
@@ -5224,24 +5416,7 @@ dialog.veil:focus {
5224
5416
  var(--reel-column-min),
5225
5417
  var(--reel-width) * (100cqw - var(--reel-gutter)) - var(--reel-gutter)
5226
5418
  );
5227
- min-height: calc(100cqh - var(--reel-gutter));
5228
- }
5229
-
5230
- /* the column's head, in the gutter above its panes: its grip and its width */
5231
- .reel-column-head {
5232
- display: flex;
5233
- flex: none;
5234
- align-items: center;
5235
- gap: var(--space-s);
5236
- padding-block: 4px;
5237
- }
5238
-
5239
- .reel-width {
5240
- font-family: var(--font-px);
5241
- font-size: var(--type-state);
5242
- letter-spacing: var(--tracking-state);
5243
- text-transform: uppercase;
5244
- color: var(--muted);
5419
+ min-height: calc(100cqh - 2 * var(--reel-gutter));
5245
5420
  }
5246
5421
 
5247
5422
  /* a slot holds a pane's place, its share of the column by weight; the pane is drawn over it */
@@ -5279,7 +5454,7 @@ dialog.veil:focus {
5279
5454
 
5280
5455
  .reel-resize::after {
5281
5456
  top: 0;
5282
- bottom: var(--reel-gutter);
5457
+ bottom: 0;
5283
5458
  left: calc(50% - 1px);
5284
5459
  width: var(--line);
5285
5460
  }
@@ -5307,7 +5482,7 @@ dialog.veil:focus {
5307
5482
  content: "";
5308
5483
  position: absolute;
5309
5484
  top: 0;
5310
- bottom: var(--reel-gutter);
5485
+ bottom: 0;
5311
5486
  left: calc(50% - 1px);
5312
5487
  border-left: var(--line) dashed var(--ink);
5313
5488
  pointer-events: none;
@@ -5377,7 +5552,7 @@ dialog.veil:focus {
5377
5552
  }
5378
5553
 
5379
5554
  /* the held copy, following the pointer from the window's corner (its transform puts it at the
5380
- pointer): the title bars alone, tinted head on the card shadow */
5555
+ pointer): the title bar alone, tinted head on the card shadow */
5381
5556
  .reel-lift {
5382
5557
  position: fixed;
5383
5558
  top: 0;
@@ -5386,12 +5561,7 @@ dialog.veil:focus {
5386
5561
  pointer-events: none;
5387
5562
  }
5388
5563
 
5389
- .reel-lifted {
5390
- display: grid;
5391
- gap: var(--space-s);
5392
- }
5393
-
5394
- .reel-lifted .window {
5564
+ .reel-lift .window {
5395
5565
  height: auto;
5396
5566
  background: var(--head);
5397
5567
  box-shadow: var(--shadow-card);
package/src/rail/index.ts CHANGED
@@ -3,11 +3,13 @@ export {
3
3
  HideRailDrawer,
4
4
  Message,
5
5
  Model,
6
+ OutMessage,
6
7
  ShowRailDrawer,
7
8
  close,
8
9
  id,
9
10
  init,
10
11
  isOpen,
12
+ subscriptions,
11
13
  update,
12
14
  view,
13
15
  } from "./rail.ts";