@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.
- package/DESIGN.md +21 -15
- package/dist/src/bench/bench.d.ts +14 -0
- package/dist/src/bench/bench.d.ts.map +1 -0
- package/dist/src/bench/bench.js +9 -0
- package/dist/src/bench/index.d.ts +3 -0
- package/dist/src/bench/index.d.ts.map +1 -0
- package/dist/src/bench/index.js +1 -0
- package/dist/src/chip/chip.d.ts +10 -0
- package/dist/src/chip/chip.d.ts.map +1 -1
- package/dist/src/chip/chip.js +19 -0
- package/dist/src/chip/index.d.ts +2 -2
- package/dist/src/chip/index.d.ts.map +1 -1
- package/dist/src/chip/index.js +1 -1
- package/dist/src/drag/drag.d.ts +18 -0
- package/dist/src/drag/drag.d.ts.map +1 -1
- package/dist/src/drag/drag.js +35 -7
- package/dist/src/field/field.d.ts +3 -0
- package/dist/src/field/field.d.ts.map +1 -1
- package/dist/src/field/field.js +2 -2
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/lineDiff/index.d.ts +2 -0
- package/dist/src/lineDiff/index.d.ts.map +1 -1
- package/dist/src/lineDiff/index.js +1 -0
- package/dist/src/lineDiff/lineDiff.d.ts +8 -4
- package/dist/src/lineDiff/lineDiff.d.ts.map +1 -1
- package/dist/src/lineDiff/lineDiff.js +19 -15
- package/dist/src/lineDiff/patch.d.ts +35 -0
- package/dist/src/lineDiff/patch.d.ts.map +1 -0
- package/dist/src/lineDiff/patch.js +146 -0
- package/dist/src/menu/menu.d.ts +6 -6
- package/dist/src/rail/index.d.ts +1 -1
- package/dist/src/rail/index.d.ts.map +1 -1
- package/dist/src/rail/index.js +1 -1
- package/dist/src/rail/rail.d.ts +217 -17
- package/dist/src/rail/rail.d.ts.map +1 -1
- package/dist/src/rail/rail.js +133 -15
- package/dist/src/reel/index.d.ts +1 -1
- package/dist/src/reel/index.d.ts.map +1 -1
- package/dist/src/reel/index.js +1 -1
- package/dist/src/reel/layout.d.ts +21 -26
- package/dist/src/reel/layout.d.ts.map +1 -1
- package/dist/src/reel/layout.js +84 -106
- package/dist/src/reel/reel.d.ts +30 -172
- package/dist/src/reel/reel.d.ts.map +1 -1
- package/dist/src/reel/reel.js +144 -195
- package/dist/src/rows/rows.d.ts +9 -1
- package/dist/src/rows/rows.d.ts.map +1 -1
- package/dist/src/rows/rows.js +1 -1
- package/dist/src/sortable/sortable.d.ts +30 -30
- package/dist/src/toast/toast.d.ts +12 -12
- package/dist/src/window/window.d.ts +3 -1
- package/dist/src/window/window.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/bench/bench.ts +24 -0
- package/src/bench/index.ts +2 -0
- package/src/chip/chip.ts +31 -0
- package/src/chip/index.ts +2 -2
- package/src/drag/drag.ts +60 -1
- package/src/field/field.ts +5 -1
- package/src/index.ts +1 -0
- package/src/lineDiff/index.ts +2 -0
- package/src/lineDiff/lineDiff.ts +29 -24
- package/src/lineDiff/patch.ts +218 -0
- package/src/pixel-paper.css +202 -32
- package/src/rail/index.ts +2 -0
- package/src/rail/rail.ts +181 -16
- package/src/reel/index.ts +0 -1
- package/src/reel/layout.ts +78 -118
- package/src/reel/reel.ts +108 -216
- package/src/rows/rows.ts +14 -2
- package/src/tokens.css +2 -1
- 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
|
+
};
|
package/src/pixel-paper.css
CHANGED
|
@@ -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(
|
|
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-
|
|
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
|
|
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:
|
|
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:
|
|
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
|
|
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-
|
|
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);
|