@gjermundgaraba-designs/pixel-paper 0.11.0 → 0.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/DESIGN.md +13 -13
- package/dist/src/emptyState/emptyState.d.ts +4 -0
- package/dist/src/emptyState/emptyState.d.ts.map +1 -1
- package/dist/src/emptyState/emptyState.js +4 -2
- package/dist/src/field/field.d.ts +14 -6
- package/dist/src/field/field.d.ts.map +1 -1
- package/dist/src/field/field.js +17 -5
- package/dist/src/fieldset/fieldset.d.ts +7 -2
- package/dist/src/fieldset/fieldset.d.ts.map +1 -1
- package/dist/src/fieldset/fieldset.js +10 -4
- package/dist/src/lineDiff/diff.d.ts +26 -31
- package/dist/src/lineDiff/diff.d.ts.map +1 -1
- package/dist/src/lineDiff/diff.js +62 -41
- package/dist/src/lineDiff/index.d.ts +2 -2
- package/dist/src/lineDiff/index.d.ts.map +1 -1
- package/dist/src/lineDiff/index.js +1 -1
- package/dist/src/lineDiff/lineDiff.d.ts +14 -0
- package/dist/src/lineDiff/lineDiff.d.ts.map +1 -1
- package/dist/src/lineDiff/lineDiff.js +43 -21
- package/dist/src/passwordField/passwordField.d.ts +11 -6
- package/dist/src/passwordField/passwordField.d.ts.map +1 -1
- package/dist/src/passwordField/passwordField.js +10 -7
- package/dist/src/places/places.d.ts +15 -5
- package/dist/src/places/places.d.ts.map +1 -1
- package/dist/src/places/places.js +19 -17
- package/dist/src/radioChoices/radioChoices.d.ts +4 -4
- package/dist/src/radioChoices/radioChoices.d.ts.map +1 -1
- package/dist/src/rail/rail.d.ts +2 -2
- package/dist/src/rail/rail.d.ts.map +1 -1
- package/dist/src/rail/rail.js +5 -3
- package/dist/src/rows/index.d.ts +1 -1
- package/dist/src/rows/index.d.ts.map +1 -1
- package/dist/src/rows/rows.d.ts +31 -5
- package/dist/src/rows/rows.d.ts.map +1 -1
- package/dist/src/rows/rows.js +46 -17
- package/dist/src/selectField/selectField.d.ts +7 -5
- package/dist/src/selectField/selectField.d.ts.map +1 -1
- package/dist/src/selectField/selectField.js +12 -6
- package/dist/src/sheet/sheet.d.ts +3 -2
- package/dist/src/sheet/sheet.d.ts.map +1 -1
- package/dist/src/sheet/sheet.js +3 -2
- package/dist/src/slot/slot.d.ts +2 -1
- package/dist/src/slot/slot.d.ts.map +1 -1
- package/dist/src/slot/slot.js +2 -2
- package/dist/src/textareaField/textareaField.d.ts +15 -5
- package/dist/src/textareaField/textareaField.d.ts.map +1 -1
- package/dist/src/textareaField/textareaField.js +15 -6
- package/package.json +1 -1
- package/src/emptyState/emptyState.ts +8 -2
- package/src/field/field.ts +37 -8
- package/src/fieldset/fieldset.ts +16 -4
- package/src/lineDiff/diff.ts +104 -48
- package/src/lineDiff/index.ts +2 -2
- package/src/lineDiff/lineDiff.ts +76 -31
- package/src/passwordField/passwordField.ts +18 -9
- package/src/pixel-paper.css +41 -6
- package/src/places/places.ts +43 -30
- package/src/radioChoices/radioChoices.ts +2 -2
- package/src/rail/rail.ts +5 -3
- package/src/rows/index.ts +1 -0
- package/src/rows/rows.ts +95 -25
- package/src/selectField/selectField.ts +17 -17
- package/src/sheet/sheet.ts +3 -2
- package/src/slot/slot.ts +4 -1
- package/src/textareaField/textareaField.ts +39 -9
package/src/lineDiff/lineDiff.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
|
-
import { Array, Option } from "effect";
|
|
1
|
+
import { Array, Option, String } from "effect";
|
|
2
2
|
import type { Html, HtmlBuilder } from "foldkit/html";
|
|
3
3
|
|
|
4
4
|
import { LOCALE } from "../locale/locale.ts";
|
|
5
5
|
import {
|
|
6
6
|
DiffRow,
|
|
7
|
-
type DiffLine,
|
|
8
7
|
type LineKind,
|
|
9
|
-
|
|
8
|
+
diffRowsOf,
|
|
9
|
+
diffRowsOfPatch,
|
|
10
10
|
foldUnchanged,
|
|
11
11
|
hasChanges,
|
|
12
12
|
} from "./diff.ts";
|
|
@@ -27,58 +27,73 @@ export type ViewConfig = Readonly<{
|
|
|
27
27
|
context?: number;
|
|
28
28
|
}>;
|
|
29
29
|
|
|
30
|
+
/** Configuration for rendering a unified diff with `patchView`. */
|
|
31
|
+
export type PatchViewConfig = Readonly<{
|
|
32
|
+
/** One file's unified diff, as `diff -u` or `git diff -- <path>` writes it. */
|
|
33
|
+
patch: string;
|
|
34
|
+
/** Names the two sides for screen readers: `"Version 3 against version 4"`. */
|
|
35
|
+
label: string;
|
|
36
|
+
}>;
|
|
37
|
+
|
|
30
38
|
const KIND: Record<LineKind, Readonly<{ className: string; sign: string; said: string }>> = {
|
|
31
39
|
Same: { className: "same", sign: "", said: "unchanged" },
|
|
32
40
|
Added: { className: "add", sign: "+", said: "added" },
|
|
33
41
|
Removed: { className: "del", sign: "−", said: "removed" },
|
|
34
42
|
};
|
|
35
43
|
|
|
36
|
-
const
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
const { className, sign, said } = KIND[line.kind];
|
|
44
|
+
const lineView = <Message>(
|
|
45
|
+
kind: LineKind,
|
|
46
|
+
numbers: Readonly<{ old: string; new: string }>,
|
|
47
|
+
text: string,
|
|
48
|
+
h: HtmlBuilder<Message>,
|
|
49
|
+
): Html => {
|
|
50
|
+
const { className, sign, said } = KIND[kind];
|
|
44
51
|
|
|
45
52
|
return h.keyed("tr")(
|
|
46
|
-
|
|
53
|
+
`${numbers.old}:${numbers.new}`,
|
|
47
54
|
[h.Class(className)],
|
|
48
55
|
[
|
|
49
|
-
h.td([h.Class("ln")], [
|
|
50
|
-
h.td([h.Class("ln")], [
|
|
56
|
+
h.td([h.Class("ln")], [numbers.old]),
|
|
57
|
+
h.td([h.Class("ln")], [numbers.new]),
|
|
51
58
|
h.td([h.Class("sign"), h.AriaLabel(said)], [sign]),
|
|
52
|
-
h.td([h.Class("line")], [
|
|
59
|
+
h.td([h.Class("line")], [text === "" ? " " : text]),
|
|
53
60
|
],
|
|
54
61
|
);
|
|
55
62
|
};
|
|
56
63
|
|
|
57
|
-
const foldView = <Message>(count: number,
|
|
64
|
+
const foldView = <Message>(count: number, firstOldNumber: number, h: HtmlBuilder<Message>): Html =>
|
|
58
65
|
h.keyed("tr")(
|
|
59
|
-
`fold-${
|
|
66
|
+
`fold-${firstOldNumber}`,
|
|
60
67
|
[h.Class("fold")],
|
|
61
|
-
[
|
|
68
|
+
[
|
|
69
|
+
h.td(
|
|
70
|
+
[h.Colspan(COLUMN_COUNT)],
|
|
71
|
+
[`${count.toLocaleString(LOCALE)} unchanged ${count === 1 ? "line" : "lines"}`],
|
|
72
|
+
),
|
|
73
|
+
],
|
|
62
74
|
);
|
|
63
75
|
|
|
64
76
|
const rowView = <Message>(row: DiffRow, h: HtmlBuilder<Message>): Html =>
|
|
65
77
|
DiffRow.match(row, {
|
|
66
|
-
|
|
67
|
-
|
|
78
|
+
Same: ({ text, oldNumber, newNumber }) =>
|
|
79
|
+
lineView("Same", { old: `${oldNumber}`, new: `${newNumber}` }, text, h),
|
|
80
|
+
Added: ({ text, newNumber }) => lineView("Added", { old: "", new: `${newNumber}` }, text, h),
|
|
81
|
+
Removed: ({ text, oldNumber }) =>
|
|
82
|
+
lineView("Removed", { old: `${oldNumber}`, new: "" }, text, h),
|
|
83
|
+
Fold: ({ count, firstOldNumber }) => foldView(count, firstOldNumber, h),
|
|
68
84
|
});
|
|
69
85
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
* named for screen readers. Two texts that are the same say so in a sentence. */
|
|
73
|
-
export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html => {
|
|
74
|
-
const { before, after, label, context = DEFAULT_CONTEXT } = config;
|
|
75
|
-
const lines = diffLinesOf(before, after);
|
|
86
|
+
const noteView = <Message>(note: string, h: HtmlBuilder<Message>): Html =>
|
|
87
|
+
h.p([h.Class("note-line")], [note]);
|
|
76
88
|
|
|
77
|
-
|
|
78
|
-
return h.p([h.Class("note-line")], ["No difference: both sides hold the same text."]);
|
|
79
|
-
}
|
|
89
|
+
const SAME_NOTE = "No difference: both sides hold the same text.";
|
|
80
90
|
|
|
81
|
-
|
|
91
|
+
const tableView = <Message>(
|
|
92
|
+
rows: ReadonlyArray<DiffRow>,
|
|
93
|
+
label: string,
|
|
94
|
+
h: HtmlBuilder<Message>,
|
|
95
|
+
): Html =>
|
|
96
|
+
h.div(
|
|
82
97
|
[h.Class("diff-wrap")],
|
|
83
98
|
[
|
|
84
99
|
h.table(
|
|
@@ -86,10 +101,40 @@ export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html
|
|
|
86
101
|
[
|
|
87
102
|
h.tbody(
|
|
88
103
|
[],
|
|
89
|
-
Array.map(
|
|
104
|
+
Array.map(rows, (row) => rowView(row, h)),
|
|
90
105
|
),
|
|
91
106
|
],
|
|
92
107
|
),
|
|
93
108
|
],
|
|
94
109
|
);
|
|
110
|
+
|
|
111
|
+
/** A line diff as a ruled table: the old and new numbers, a sign, the line. Added lines sit on
|
|
112
|
+
* the head tint behind an ink +; removed lines go muted behind a red −, and each sign is
|
|
113
|
+
* named for screen readers. Two texts that are the same say so in a sentence. */
|
|
114
|
+
export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html => {
|
|
115
|
+
const { before, after, label, context = DEFAULT_CONTEXT } = config;
|
|
116
|
+
const rows = diffRowsOf(before, after);
|
|
117
|
+
|
|
118
|
+
return hasChanges(rows)
|
|
119
|
+
? tableView(foldUnchanged(rows, context), label, h)
|
|
120
|
+
: noteView(SAME_NOTE, h);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
/** A unified diff the caller already has (from a server, or `git diff -- <path>`) as `view`'s
|
|
124
|
+
* table: its hunks' lines, numbered from their headers, and the unchanged lines between hunks
|
|
125
|
+
* folded into a counted row. Lines past the last hunk are not in the patch, so nothing counts
|
|
126
|
+
* them. An empty patch says there is no difference, as `view` does; anything but one file
|
|
127
|
+
* with a hunk (text that is not a diff, a binary change, several files) says it cannot be
|
|
128
|
+
* shown as lines. */
|
|
129
|
+
export const patchView = <Message>(config: PatchViewConfig, h: HtmlBuilder<Message>): Html => {
|
|
130
|
+
const { patch, label } = config;
|
|
131
|
+
|
|
132
|
+
if (String.isEmpty(String.trim(patch))) {
|
|
133
|
+
return noteView(SAME_NOTE, h);
|
|
134
|
+
} else {
|
|
135
|
+
return Option.match(diffRowsOfPatch(patch), {
|
|
136
|
+
onNone: () => noteView("This diff cannot be shown as lines.", h),
|
|
137
|
+
onSome: (rows) => tableView(rows, label, h),
|
|
138
|
+
});
|
|
139
|
+
}
|
|
95
140
|
};
|
|
@@ -3,6 +3,7 @@ import { Match, Schema } from "effect";
|
|
|
3
3
|
import type { Html, HtmlBuilder } from "foldkit/html";
|
|
4
4
|
|
|
5
5
|
import { pressedAttribute } from "../button/button.ts";
|
|
6
|
+
import { describedByError, errorView } from "../field/field.ts";
|
|
6
7
|
import * as IconButton from "../iconButton/iconButton.ts";
|
|
7
8
|
import * as Meter from "../meter/meter.ts";
|
|
8
9
|
|
|
@@ -41,8 +42,10 @@ export type ViewConfig<Message> = Readonly<{
|
|
|
41
42
|
onToggleShown: Message;
|
|
42
43
|
/** A sans sentence under the box. */
|
|
43
44
|
help?: string;
|
|
44
|
-
/**
|
|
45
|
-
|
|
45
|
+
/** Why the value was refused: a red sentence under the control, which the control is
|
|
46
|
+
* described by, with red on its box and its label (`aria-invalid`). Say it again in an
|
|
47
|
+
* ErrorSummary above the form. */
|
|
48
|
+
error?: string | undefined;
|
|
46
49
|
/** The form is not sent without it: screen readers hear "required" (`aria-required`). */
|
|
47
50
|
isRequired?: boolean;
|
|
48
51
|
/** For a new password: four short cells and a word under the box (an empty box is "too
|
|
@@ -50,13 +53,16 @@ export type ViewConfig<Message> = Readonly<{
|
|
|
50
53
|
strength?: Strength;
|
|
51
54
|
/** `current-password` to sign in, `new-password` to set one. */
|
|
52
55
|
autocomplete?: "current-password" | "new-password";
|
|
56
|
+
/** The show toggle's name, "Show password" when not given: give each of two fields in a
|
|
57
|
+
* form its own, such as "Show password again". */
|
|
58
|
+
toggleLabel?: string;
|
|
53
59
|
}>;
|
|
54
60
|
|
|
55
61
|
/** A boxed password field with a show toggle inside its box, built on `@foldkit/ui`'s Input:
|
|
56
|
-
* named by its label, described by its help line, `aria-invalid`
|
|
57
|
-
* `aria-required` when the form is not sent without it. The
|
|
58
|
-
* password", `aria-pressed` while the
|
|
59
|
-
* meter. */
|
|
62
|
+
* named by its label, described by its help line, `aria-invalid` and described by its
|
|
63
|
+
* `error` when it failed, and `aria-required` when the form is not sent without it. The
|
|
64
|
+
* toggle is a button named `toggleLabel` ("Show password"), `aria-pressed` while the
|
|
65
|
+
* password shows. For a new password, its strength is a meter. */
|
|
60
66
|
export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Message>): Html => {
|
|
61
67
|
const {
|
|
62
68
|
id,
|
|
@@ -66,10 +72,11 @@ export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Messag
|
|
|
66
72
|
isShown,
|
|
67
73
|
onToggleShown,
|
|
68
74
|
help,
|
|
69
|
-
|
|
75
|
+
error,
|
|
70
76
|
isRequired = false,
|
|
71
77
|
strength,
|
|
72
78
|
autocomplete = "current-password",
|
|
79
|
+
toggleLabel = "Show password",
|
|
73
80
|
} = config;
|
|
74
81
|
|
|
75
82
|
return UiInput.view(
|
|
@@ -77,7 +84,7 @@ export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Messag
|
|
|
77
84
|
id,
|
|
78
85
|
value,
|
|
79
86
|
onInput,
|
|
80
|
-
isInvalid,
|
|
87
|
+
isInvalid: error !== undefined,
|
|
81
88
|
type: isShown ? "text" : "password",
|
|
82
89
|
hasDescription: help !== undefined,
|
|
83
90
|
toView: (groups) =>
|
|
@@ -92,12 +99,13 @@ export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Messag
|
|
|
92
99
|
h.input([
|
|
93
100
|
...groups.input,
|
|
94
101
|
h.Autocomplete(autocomplete),
|
|
102
|
+
...describedByError(id, error, help !== undefined, h),
|
|
95
103
|
...(isRequired ? [h.AriaRequired(true)] : []),
|
|
96
104
|
]),
|
|
97
105
|
IconButton.view(
|
|
98
106
|
{
|
|
99
107
|
glyph: isShown ? "eye-off" : "eye",
|
|
100
|
-
label:
|
|
108
|
+
label: toggleLabel,
|
|
101
109
|
isQuiet: true,
|
|
102
110
|
onClick: onToggleShown,
|
|
103
111
|
attributes: [pressedAttribute(isShown, h)],
|
|
@@ -106,6 +114,7 @@ export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Messag
|
|
|
106
114
|
),
|
|
107
115
|
],
|
|
108
116
|
),
|
|
117
|
+
...errorView(id, error, h),
|
|
109
118
|
...(strength === undefined
|
|
110
119
|
? []
|
|
111
120
|
: [
|
package/src/pixel-paper.css
CHANGED
|
@@ -924,7 +924,9 @@ a.textbtn {
|
|
|
924
924
|
min-width: 0;
|
|
925
925
|
}
|
|
926
926
|
|
|
927
|
-
|
|
927
|
+
/* a sheet takes a panel's place on the page, so it prints as one */
|
|
928
|
+
.panel,
|
|
929
|
+
.sheet {
|
|
928
930
|
background: var(--paper);
|
|
929
931
|
border: var(--line) solid var(--ink);
|
|
930
932
|
box-shadow: var(--shadow-panel);
|
|
@@ -1354,7 +1356,8 @@ button.glyph {
|
|
|
1354
1356
|
margin: -4px;
|
|
1355
1357
|
}
|
|
1356
1358
|
|
|
1357
|
-
/* an inline edit in a row shows its focus as its caret and its rule, not a
|
|
1359
|
+
/* an inline edit in a row shows its focus as its caret and its rule, thickened below, not a
|
|
1360
|
+
ring */
|
|
1358
1361
|
.row .grow input.inline-input:focus-visible {
|
|
1359
1362
|
outline: none;
|
|
1360
1363
|
}
|
|
@@ -1398,6 +1401,20 @@ button.glyph {
|
|
|
1398
1401
|
opacity: 1;
|
|
1399
1402
|
}
|
|
1400
1403
|
|
|
1404
|
+
/* a treegrid's own cells around the parts the caller gives (the glyph, the actions): they hold
|
|
1405
|
+
their parts as the row would, so a row lays out the same in a treegrid or out of one */
|
|
1406
|
+
.row > .cell {
|
|
1407
|
+
display: inline-flex;
|
|
1408
|
+
align-items: center;
|
|
1409
|
+
gap: 8px;
|
|
1410
|
+
flex: none;
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
.row > .grow-cell {
|
|
1414
|
+
flex: 1;
|
|
1415
|
+
min-width: 0;
|
|
1416
|
+
}
|
|
1417
|
+
|
|
1401
1418
|
.row .slot {
|
|
1402
1419
|
min-width: 58px;
|
|
1403
1420
|
flex: none;
|
|
@@ -1526,7 +1543,7 @@ button.glyph {
|
|
|
1526
1543
|
inset: 0;
|
|
1527
1544
|
}
|
|
1528
1545
|
|
|
1529
|
-
.row
|
|
1546
|
+
.row label.grow {
|
|
1530
1547
|
cursor: pointer;
|
|
1531
1548
|
}
|
|
1532
1549
|
|
|
@@ -1611,6 +1628,17 @@ input.inline-input[aria-invalid="true"] {
|
|
|
1611
1628
|
border-bottom-color: var(--red);
|
|
1612
1629
|
}
|
|
1613
1630
|
|
|
1631
|
+
/* in a row, which takes its ring, focus doubles the rule to 2px, as a field's baseline does;
|
|
1632
|
+
drawn inside the box, so the row keeps its height, and in the rule's own colour, so a
|
|
1633
|
+
refused edit stays red */
|
|
1634
|
+
.row .grow input.inline-input:focus-visible {
|
|
1635
|
+
box-shadow: inset 0 -1px 0 var(--ink);
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
.row .grow input.inline-input[aria-invalid="true"]:focus-visible {
|
|
1639
|
+
box-shadow: inset 0 -1px 0 var(--red);
|
|
1640
|
+
}
|
|
1641
|
+
|
|
1614
1642
|
/* the flash word: a transient confirmation printed beside the action, then gone */
|
|
1615
1643
|
.flash-word {
|
|
1616
1644
|
font-family: var(--font-px);
|
|
@@ -1853,12 +1881,18 @@ textarea::-webkit-resizer {
|
|
|
1853
1881
|
color: var(--red);
|
|
1854
1882
|
}
|
|
1855
1883
|
|
|
1856
|
-
.field .field-help
|
|
1884
|
+
.field .field-help,
|
|
1885
|
+
.field .field-error {
|
|
1857
1886
|
margin-top: 4px;
|
|
1858
1887
|
font: var(--type-control)/1.45 var(--font-body);
|
|
1859
1888
|
color: var(--muted);
|
|
1860
1889
|
}
|
|
1861
1890
|
|
|
1891
|
+
/* why the field failed, said in red ink under the control and before its help */
|
|
1892
|
+
.field .field-error {
|
|
1893
|
+
color: var(--red);
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1862
1896
|
.field-grid {
|
|
1863
1897
|
display: grid;
|
|
1864
1898
|
grid-template-columns: repeat(auto-fit, minmax(160px, 1fr));
|
|
@@ -2961,7 +2995,8 @@ dialog.veil:focus {
|
|
|
2961
2995
|
justify-items: center;
|
|
2962
2996
|
}
|
|
2963
2997
|
|
|
2964
|
-
.empty
|
|
2998
|
+
.empty-title {
|
|
2999
|
+
margin: 0;
|
|
2965
3000
|
font-family: var(--font-px);
|
|
2966
3001
|
font-size: var(--type-control);
|
|
2967
3002
|
text-transform: uppercase;
|
|
@@ -3971,7 +4006,7 @@ dialog.veil:focus {
|
|
|
3971
4006
|
outline-offset: 2px;
|
|
3972
4007
|
}
|
|
3973
4008
|
|
|
3974
|
-
/* ---------- the reading sheet: an authored page
|
|
4009
|
+
/* ---------- the reading sheet: an authored page in a panel's place ---------- */
|
|
3975
4010
|
|
|
3976
4011
|
.sheet {
|
|
3977
4012
|
padding: 28px 32px 36px;
|
package/src/places/places.ts
CHANGED
|
@@ -7,8 +7,11 @@ import { slotView } from "../slot/slot.ts";
|
|
|
7
7
|
|
|
8
8
|
// VIEW
|
|
9
9
|
|
|
10
|
-
/** One place in Places: a button that shows it
|
|
11
|
-
|
|
10
|
+
/** One place in Places: a link to its address (`href`), or a button that shows it in the page
|
|
11
|
+
* (`onSelect`). */
|
|
12
|
+
export type Place<Message> = PlaceContent & PlaceTarget<Message>;
|
|
13
|
+
|
|
14
|
+
type PlaceContent = Readonly<{
|
|
12
15
|
/** Stable and unique among the places: the row's key, so two places that share a name stay
|
|
13
16
|
* apart. */
|
|
14
17
|
id: string;
|
|
@@ -20,10 +23,20 @@ export type Place<Message> = Readonly<{
|
|
|
20
23
|
count?: number;
|
|
21
24
|
/** A quiet IconButton after it that acts on the place itself (remove a saved filter). */
|
|
22
25
|
tools?: ReadonlyArray<Html>;
|
|
23
|
-
/** Shows the place. */
|
|
24
|
-
onSelect: Message;
|
|
25
26
|
}>;
|
|
26
27
|
|
|
28
|
+
type PlaceTarget<Message> =
|
|
29
|
+
| Readonly<{
|
|
30
|
+
/** The place's address: the line is a link, so it opens in a new tab and reads as one. */
|
|
31
|
+
href: string;
|
|
32
|
+
onSelect?: never;
|
|
33
|
+
}>
|
|
34
|
+
| Readonly<{
|
|
35
|
+
/** Shows the place without leaving the page: the line is a button. */
|
|
36
|
+
onSelect: Message;
|
|
37
|
+
href?: never;
|
|
38
|
+
}>;
|
|
39
|
+
|
|
27
40
|
/** Configuration for rendering places with `view`. */
|
|
28
41
|
export type ViewConfig<Message> = Readonly<{
|
|
29
42
|
/** Names the navigation for screen readers ("Areas"). */
|
|
@@ -31,39 +44,39 @@ export type ViewConfig<Message> = Readonly<{
|
|
|
31
44
|
places: ReadonlyArray<Place<Message>>;
|
|
32
45
|
}>;
|
|
33
46
|
|
|
47
|
+
const lineView = <Message>(place: Place<Message>, h: HtmlBuilder<Message>): Html => {
|
|
48
|
+
const { label, isCurrent = false, count } = place;
|
|
49
|
+
const current = isCurrent ? [h.AriaCurrent("page")] : [];
|
|
50
|
+
const content = [
|
|
51
|
+
h.span([h.Class("place-name")], [label]),
|
|
52
|
+
...(count === undefined
|
|
53
|
+
? []
|
|
54
|
+
: [" ", h.span([h.Class("place-count")], [count.toLocaleString(LOCALE)])]),
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
return place.href === undefined
|
|
58
|
+
? UiButton.view(
|
|
59
|
+
{
|
|
60
|
+
onClick: place.onSelect,
|
|
61
|
+
toView: (groups) => h.button([...groups.button, ...current], content),
|
|
62
|
+
},
|
|
63
|
+
h,
|
|
64
|
+
)
|
|
65
|
+
: h.a([h.Href(place.href), ...current], content);
|
|
66
|
+
};
|
|
67
|
+
|
|
34
68
|
const placeView =
|
|
35
69
|
<Message>(h: HtmlBuilder<Message>) =>
|
|
36
|
-
(place: Place<Message>): Html =>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return h.keyed("li")(
|
|
70
|
+
(place: Place<Message>): Html =>
|
|
71
|
+
h.keyed("li")(
|
|
40
72
|
place.id,
|
|
41
73
|
[],
|
|
42
|
-
[
|
|
43
|
-
UiButton.view(
|
|
44
|
-
{
|
|
45
|
-
onClick: onSelect,
|
|
46
|
-
toView: (groups) =>
|
|
47
|
-
h.button(
|
|
48
|
-
[...groups.button, ...(isCurrent ? [h.AriaCurrent("page")] : [])],
|
|
49
|
-
[
|
|
50
|
-
h.span([h.Class("place-name")], [label]),
|
|
51
|
-
...(count === undefined
|
|
52
|
-
? []
|
|
53
|
-
: [" ", h.span([h.Class("place-count")], [count.toLocaleString(LOCALE)])]),
|
|
54
|
-
],
|
|
55
|
-
),
|
|
56
|
-
},
|
|
57
|
-
h,
|
|
58
|
-
),
|
|
59
|
-
...slotView("span", "place-tools", tools, h),
|
|
60
|
-
],
|
|
74
|
+
[lineView(place, h), ...slotView("span", "place-tools", place.tools ?? [], h)],
|
|
61
75
|
);
|
|
62
|
-
};
|
|
63
76
|
|
|
64
77
|
/** A column of places to go (areas, collections, saved filters), flush in a rail panel: a
|
|
65
|
-
* navigation landmark named by its label, one line each a button built
|
|
66
|
-
* Button, the one you are in `aria-current="page"` and printed inverted. */
|
|
78
|
+
* navigation landmark named by its label, one line each a link to the place or a button built
|
|
79
|
+
* on `@foldkit/ui`'s Button, the one you are in `aria-current="page"` and printed inverted. */
|
|
67
80
|
export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Message>): Html => {
|
|
68
81
|
const { label, places } = config;
|
|
69
82
|
|
|
@@ -83,8 +83,8 @@ export type ViewInputs = Readonly<{
|
|
|
83
83
|
options: ReadonlyArray<RadioOption>;
|
|
84
84
|
/** The parent's selection. */
|
|
85
85
|
selectedValue: Option.Option<string>;
|
|
86
|
-
/** The group is `aria-invalid` and its marks and names turn red; say why in
|
|
87
|
-
* ErrorSummary above the form. */
|
|
86
|
+
/** The group is `aria-invalid` and its marks and names turn red; say why in its
|
|
87
|
+
* Fieldset's `error` and in an ErrorSummary above the form. */
|
|
88
88
|
isInvalid?: boolean;
|
|
89
89
|
}>;
|
|
90
90
|
|
package/src/rail/rail.ts
CHANGED
|
@@ -234,8 +234,8 @@ export type ViewInputs = Readonly<{
|
|
|
234
234
|
|
|
235
235
|
/**
|
|
236
236
|
* A column of panels beside the deck, fixed under the top bar and scrolling itself:
|
|
237
|
-
* `div.railed
|
|
238
|
-
* layouts, so nothing in it renders twice or loses its state. At 900px and under it leaves the
|
|
237
|
+
* `div.railed >` the deck, then `aside.rail[popover="auto"][aria-label]`. One element serves
|
|
238
|
+
* both layouts, so nothing in it renders twice or loses its state. At 900px and under it leaves the
|
|
239
239
|
* page, and `span.rail-open > button.btn.small` at the top of the deck opens the same element as a
|
|
240
240
|
* drawer over a veil, its head (the name and a close glyph) showing. Opening focuses its first
|
|
241
241
|
* control; Escape, a press on the veil, the close glyph, focus leaving or the window growing
|
|
@@ -268,9 +268,12 @@ export const view = defineView<Model, Message, ViewInputs>((model, viewInputs, h
|
|
|
268
268
|
],
|
|
269
269
|
);
|
|
270
270
|
|
|
271
|
+
// NOTE: the deck comes first so the page's main content, and its h1, precede the rail's
|
|
272
|
+
// panels in reading order; the stylesheet places the rail, fixed, at the left.
|
|
271
273
|
return h.div(
|
|
272
274
|
[h.Class("railed")],
|
|
273
275
|
[
|
|
276
|
+
toDeckView(openButton),
|
|
274
277
|
h.aside(
|
|
275
278
|
[
|
|
276
279
|
h.Id(model.id),
|
|
@@ -293,7 +296,6 @@ export const view = defineView<Model, Message, ViewInputs>((model, viewInputs, h
|
|
|
293
296
|
...toAsideView(),
|
|
294
297
|
],
|
|
295
298
|
),
|
|
296
|
-
toDeckView(openButton),
|
|
297
299
|
],
|
|
298
300
|
);
|
|
299
301
|
});
|