@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.
Files changed (65) hide show
  1. package/DESIGN.md +13 -13
  2. package/dist/src/emptyState/emptyState.d.ts +4 -0
  3. package/dist/src/emptyState/emptyState.d.ts.map +1 -1
  4. package/dist/src/emptyState/emptyState.js +4 -2
  5. package/dist/src/field/field.d.ts +14 -6
  6. package/dist/src/field/field.d.ts.map +1 -1
  7. package/dist/src/field/field.js +17 -5
  8. package/dist/src/fieldset/fieldset.d.ts +7 -2
  9. package/dist/src/fieldset/fieldset.d.ts.map +1 -1
  10. package/dist/src/fieldset/fieldset.js +10 -4
  11. package/dist/src/lineDiff/diff.d.ts +26 -31
  12. package/dist/src/lineDiff/diff.d.ts.map +1 -1
  13. package/dist/src/lineDiff/diff.js +62 -41
  14. package/dist/src/lineDiff/index.d.ts +2 -2
  15. package/dist/src/lineDiff/index.d.ts.map +1 -1
  16. package/dist/src/lineDiff/index.js +1 -1
  17. package/dist/src/lineDiff/lineDiff.d.ts +14 -0
  18. package/dist/src/lineDiff/lineDiff.d.ts.map +1 -1
  19. package/dist/src/lineDiff/lineDiff.js +43 -21
  20. package/dist/src/passwordField/passwordField.d.ts +11 -6
  21. package/dist/src/passwordField/passwordField.d.ts.map +1 -1
  22. package/dist/src/passwordField/passwordField.js +10 -7
  23. package/dist/src/places/places.d.ts +15 -5
  24. package/dist/src/places/places.d.ts.map +1 -1
  25. package/dist/src/places/places.js +19 -17
  26. package/dist/src/radioChoices/radioChoices.d.ts +4 -4
  27. package/dist/src/radioChoices/radioChoices.d.ts.map +1 -1
  28. package/dist/src/rail/rail.d.ts +2 -2
  29. package/dist/src/rail/rail.d.ts.map +1 -1
  30. package/dist/src/rail/rail.js +5 -3
  31. package/dist/src/rows/index.d.ts +1 -1
  32. package/dist/src/rows/index.d.ts.map +1 -1
  33. package/dist/src/rows/rows.d.ts +31 -5
  34. package/dist/src/rows/rows.d.ts.map +1 -1
  35. package/dist/src/rows/rows.js +46 -17
  36. package/dist/src/selectField/selectField.d.ts +7 -5
  37. package/dist/src/selectField/selectField.d.ts.map +1 -1
  38. package/dist/src/selectField/selectField.js +12 -6
  39. package/dist/src/sheet/sheet.d.ts +3 -2
  40. package/dist/src/sheet/sheet.d.ts.map +1 -1
  41. package/dist/src/sheet/sheet.js +3 -2
  42. package/dist/src/slot/slot.d.ts +2 -1
  43. package/dist/src/slot/slot.d.ts.map +1 -1
  44. package/dist/src/slot/slot.js +2 -2
  45. package/dist/src/textareaField/textareaField.d.ts +15 -5
  46. package/dist/src/textareaField/textareaField.d.ts.map +1 -1
  47. package/dist/src/textareaField/textareaField.js +15 -6
  48. package/package.json +1 -1
  49. package/src/emptyState/emptyState.ts +8 -2
  50. package/src/field/field.ts +37 -8
  51. package/src/fieldset/fieldset.ts +16 -4
  52. package/src/lineDiff/diff.ts +104 -48
  53. package/src/lineDiff/index.ts +2 -2
  54. package/src/lineDiff/lineDiff.ts +76 -31
  55. package/src/passwordField/passwordField.ts +18 -9
  56. package/src/pixel-paper.css +41 -6
  57. package/src/places/places.ts +43 -30
  58. package/src/radioChoices/radioChoices.ts +2 -2
  59. package/src/rail/rail.ts +5 -3
  60. package/src/rows/index.ts +1 -0
  61. package/src/rows/rows.ts +95 -25
  62. package/src/selectField/selectField.ts +17 -17
  63. package/src/sheet/sheet.ts +3 -2
  64. package/src/slot/slot.ts +4 -1
  65. package/src/textareaField/textareaField.ts +39 -9
@@ -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
- diffLinesOf,
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 numberText = (maybeNumber: Option.Option<number>): string =>
37
- Option.match(maybeNumber, { onNone: () => "", onSome: (lineNumber) => `${lineNumber}` });
38
-
39
- const lineKey = (line: DiffLine): string =>
40
- `${numberText(line.maybeOldNumber)}:${numberText(line.maybeNewNumber)}`;
41
-
42
- const lineView = <Message>(line: DiffLine, h: HtmlBuilder<Message>): Html => {
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
- lineKey(line),
53
+ `${numbers.old}:${numbers.new}`,
47
54
  [h.Class(className)],
48
55
  [
49
- h.td([h.Class("ln")], [numberText(line.maybeOldNumber)]),
50
- h.td([h.Class("ln")], [numberText(line.maybeNewNumber)]),
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")], [line.text === "" ? " " : line.text]),
59
+ h.td([h.Class("line")], [text === "" ? " " : text]),
53
60
  ],
54
61
  );
55
62
  };
56
63
 
57
- const foldView = <Message>(count: number, firstLine: DiffLine, h: HtmlBuilder<Message>): Html =>
64
+ const foldView = <Message>(count: number, firstOldNumber: number, h: HtmlBuilder<Message>): Html =>
58
65
  h.keyed("tr")(
59
- `fold-${lineKey(firstLine)}`,
66
+ `fold-${firstOldNumber}`,
60
67
  [h.Class("fold")],
61
- [h.td([h.Colspan(COLUMN_COUNT)], [`${count.toLocaleString(LOCALE)} unchanged lines`])],
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
- Line: ({ line }) => lineView(line, h),
67
- Fold: ({ count, firstLine }) => foldView(count, firstLine, h),
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
- /** A line diff as a ruled table: the old and new numbers, a sign, the line. Added lines sit on
71
- * the head tint behind an ink +; removed lines go muted behind a red −, and each sign is
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
- if (!hasChanges(lines)) {
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
- return h.div(
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(foldUnchanged(lines, context), (row) => rowView(row, h)),
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
- /** Red on the box and on its label; say why in an ErrorSummary above the form. */
45
- isInvalid?: boolean;
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` when it failed and
57
- * `aria-required` when the form is not sent without it. The toggle is a button named "Show
58
- * password", `aria-pressed` while the password shows. For a new password, its strength is a
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
- isInvalid = false,
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: "Show password",
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
  : [
@@ -924,7 +924,9 @@ a.textbtn {
924
924
  min-width: 0;
925
925
  }
926
926
 
927
- .panel {
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 ring */
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 > label.grow {
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 strong {
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 inside a panel ---------- */
4009
+ /* ---------- the reading sheet: an authored page in a panel's place ---------- */
3975
4010
 
3976
4011
  .sheet {
3977
4012
  padding: 28px 32px 36px;
@@ -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
- export type Place<Message> = Readonly<{
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
- const { label, isCurrent = false, count, tools = [], onSelect } = place;
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 on `@foldkit/ui`'s
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 an
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 > aside.rail[popover="auto"][aria-label]` and the deck. One element serves both
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
  });
package/src/rows/index.ts CHANGED
@@ -5,5 +5,6 @@ export type {
5
5
  RowSelect,
6
6
  TitleConfig,
7
7
  ToggleConfig,
8
+ TreegridPlace,
8
9
  ViewConfig,
9
10
  } from "./rows.ts";