@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
package/src/rows/rows.ts CHANGED
@@ -14,14 +14,19 @@ export type ViewConfig = Readonly<{
14
14
  rows: ReadonlyArray<Html>;
15
15
  /** An outline: nested rows indent 26px and hang on a 1px rule guide. */
16
16
  isOutline?: boolean;
17
- /** Names the list for screen readers. */
17
+ /** Names the list for screen readers; a treegrid needs it. */
18
18
  label?: string;
19
+ /** A keyboard-driven list, whose cursor the application moves (see Keys in DESIGN.md): a
20
+ * `role="treegrid"`, so a screen reader hands it the arrow and letter keys. Give every row
21
+ * its `treegrid` place. */
22
+ isTreegrid?: boolean;
19
23
  }>;
20
24
 
21
25
  /** A list of 34px rows. */
22
26
  export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html =>
23
27
  h.ul(
24
28
  [
29
+ ...(config.isTreegrid === true ? [h.Role("treegrid")] : []),
25
30
  ...(config.label === undefined ? [] : [h.AriaLabel(config.label)]),
26
31
  h.Class(config.isOutline === true ? "rows tree" : "rows"),
27
32
  ],
@@ -45,6 +50,20 @@ export type RowSelect<Message> = Readonly<{
45
50
  label?: string;
46
51
  }>;
47
52
 
53
+ /** A row's place in a treegrid (`view`'s `isTreegrid`): its line is a `role="row"` and each
54
+ * of its parts a `role="gridcell"`, while the list items around them step aside
55
+ * (`role="none"`). */
56
+ export type TreegridPlace = Readonly<{
57
+ /** Its depth, from 1 at the list's roots: `aria-level`. */
58
+ level: number;
59
+ /** Its place among its siblings, from 1: `aria-posinset`. */
60
+ position: number;
61
+ /** How many siblings it has, itself included: `aria-setsize`. */
62
+ setSize: number;
63
+ /** Whether the rows under it show, for a row that folds: `aria-expanded`. */
64
+ isExpanded?: boolean;
65
+ }>;
66
+
48
67
  /** Configuration for rendering a row with `rowView`. */
49
68
  export type RowConfig<Message> = Readonly<{
50
69
  /** The row's stable id among its siblings. */
@@ -55,6 +74,10 @@ export type RowConfig<Message> = Readonly<{
55
74
  body: ReadonlyArray<Html | string>;
56
75
  /** Right-aligned chips, marks, counts, due days. */
57
76
  slot?: ReadonlyArray<Html | string>;
77
+ /** Further attributes for the slot's `span.slot`, such as an id the row's line is described
78
+ * by (`aria-describedby`); there is no span, and so nothing to point at, while `slot` is
79
+ * empty. Never a class. */
80
+ slotAttributes?: Attributes<Message>;
58
81
  /** Controls that belong to the row's job, before the tools (an approve and reject pair). */
59
82
  actions?: ReadonlyArray<Html | string>;
60
83
  /** Icon buttons at the row's right end, dimmed until the row is hovered or focused (full
@@ -68,12 +91,45 @@ export type RowConfig<Message> = Readonly<{
68
91
  isContext?: boolean;
69
92
  /** Makes the row selectable: a box at its left edge. */
70
93
  select?: RowSelect<Message>;
94
+ /** The row's place in a treegrid; every row of a treegrid has one. */
95
+ treegrid?: TreegridPlace;
71
96
  /** Further attributes for the row's line, the `div.row`, not its `li`: a keyboard list's
72
97
  * cursor (`aria-current="true"`, which the stylesheet outlines, and a roving `tabindex`),
73
- * `role="group"` and a name, data attributes, key and focus handlers. Never a class. */
98
+ * its name (with `role="group"` outside a treegrid, whose row is already a named `row`), data
99
+ * attributes, key and focus handlers. Never a class. */
74
100
  attributes?: Attributes<Message>;
75
101
  }>;
76
102
 
103
+ const cellRole = <Message>(
104
+ config: RowConfig<Message>,
105
+ h: HtmlBuilder<Message>,
106
+ ): Attributes<Message> => (config.treegrid === undefined ? [] : [h.Role("gridcell")]);
107
+
108
+ const rowRole = <Message>(
109
+ place: TreegridPlace | undefined,
110
+ h: HtmlBuilder<Message>,
111
+ ): Attributes<Message> =>
112
+ place === undefined
113
+ ? []
114
+ : [
115
+ h.Role("row"),
116
+ h.AriaLevel(place.level),
117
+ h.AriaPosinset(place.position),
118
+ h.AriaSetsize(place.setSize),
119
+ ...(place.isExpanded === undefined ? [] : [h.AriaExpanded(place.isExpanded)]),
120
+ ];
121
+
122
+ /** A part the caller gives (a glyph, the actions) in its own cell, in a treegrid. */
123
+ const cellView = <Message>(
124
+ config: RowConfig<Message>,
125
+ className: string,
126
+ content: ReadonlyArray<Html | string>,
127
+ h: HtmlBuilder<Message>,
128
+ ): ReadonlyArray<Html | string> =>
129
+ config.treegrid === undefined
130
+ ? content
131
+ : slotView("span", className, content, h, cellRole(config, h));
132
+
77
133
  /** The row's text, with the words that say a context row is one. */
78
134
  const textOf = <Message>(
79
135
  config: RowConfig<Message>,
@@ -90,14 +146,21 @@ const lineView = <Message>(
90
146
  h: HtmlBuilder<Message>,
91
147
  ): Html =>
92
148
  h.div(
93
- [...(config.attributes ?? []), h.Class(config.isContext === true ? "row context" : "row")],
149
+ [
150
+ ...(config.attributes ?? []),
151
+ ...rowRole(config.treegrid, h),
152
+ h.Class(config.isContext === true ? "row context" : "row"),
153
+ ],
94
154
  [
95
155
  ...box,
96
- ...(config.glyph === undefined ? [] : [config.glyph]),
156
+ ...cellView(config, "cell", config.glyph === undefined ? [] : [config.glyph], h),
97
157
  text,
98
- ...slotView("span", "slot", config.slot ?? [], h),
99
- ...(config.actions ?? []),
100
- ...slotView("span", "tools", config.tools ?? [], h),
158
+ ...slotView("span", "slot", config.slot ?? [], h, [
159
+ ...(config.slotAttributes ?? []),
160
+ ...cellRole(config, h),
161
+ ]),
162
+ ...cellView(config, "cell actions", config.actions ?? [], h),
163
+ ...slotView("span", "tools", config.tools ?? [], h, cellRole(config, h)),
101
164
  ],
102
165
  );
103
166
 
@@ -113,19 +176,25 @@ const selectableLineView = <Message>(
113
176
  onToggle: select.onToggle,
114
177
  toView: (groups) => {
115
178
  const boxView = (name: ReadonlyArray<Html>): Html =>
116
- h.span([h.Class("row-check")], [h.button(groups.checkbox), ...name]);
179
+ h.span(
180
+ [...cellRole(config, h), h.Class("row-check")],
181
+ [h.button(groups.checkbox), ...name],
182
+ );
183
+ const label = h.label([...groups.label, h.Class("grow")], textOf(config, h));
117
184
 
118
185
  return select.label === undefined
119
186
  ? lineView(
120
187
  config,
121
188
  [boxView([])],
122
- h.label([...groups.label, h.Class("grow")], textOf(config, h)),
189
+ config.treegrid === undefined
190
+ ? label
191
+ : h.span([...cellRole(config, h), h.Class("cell grow-cell")], [label]),
123
192
  h,
124
193
  )
125
194
  : lineView(
126
195
  config,
127
196
  [boxView([h.span([...groups.label, h.Class("sr-only")], [select.label])])],
128
- h.div([h.Class("grow")], textOf(config, h)),
197
+ h.div([...cellRole(config, h), h.Class("grow")], textOf(config, h)),
129
198
  h,
130
199
  );
131
200
  },
@@ -135,18 +204,18 @@ const selectableLineView = <Message>(
135
204
 
136
205
  /** One 34px row: box, glyph, text, slot, actions, tools, and the rows nested under it. Hover
137
206
  * and focus tint it head. The box of a selectable row is built on `@foldkit/ui`'s Checkbox:
138
- * a `role="checkbox"` button (`aria-checked`) whose whole cell is its target. */
139
- export const rowView = <Message>(config: RowConfig<Message>, h: HtmlBuilder<Message>): Html =>
140
- h.keyed("li")(
141
- config.key,
142
- [],
143
- [
144
- config.select === undefined
145
- ? lineView(config, [], h.div([h.Class("grow")], textOf(config, h)), h)
146
- : selectableLineView(config, config.select, h),
147
- ...slotView("ul", "rows", config.nested ?? [], h),
148
- ],
149
- );
207
+ * a `role="checkbox"` button (`aria-checked`) whose whole cell is its target. In a treegrid
208
+ * the line is the `role="row"` and each part a `role="gridcell"`. */
209
+ export const rowView = <Message>(config: RowConfig<Message>, h: HtmlBuilder<Message>): Html => {
210
+ const listRole = config.treegrid === undefined ? [] : [h.Role("none")];
211
+
212
+ return h.keyed("li")(config.key, listRole, [
213
+ config.select === undefined
214
+ ? lineView(config, [], h.div([...cellRole(config, h), h.Class("grow")], textOf(config, h)), h)
215
+ : selectableLineView(config, config.select, h),
216
+ ...slotView("ul", "rows", config.nested ?? [], h, listRole),
217
+ ]);
218
+ };
150
219
 
151
220
  // GLYPH SLOT
152
221
 
@@ -162,13 +231,14 @@ export const glyphView = <Message>(config: GlyphConfig, h: HtmlBuilder<Message>)
162
231
  /** Configuration for rendering a fold toggle with `toggleView`. */
163
232
  export type ToggleConfig<Message> = Readonly<{
164
233
  isExpanded: boolean;
165
- /** What folds: `"Fold Notes"` and `"Unfold Notes"` are built from it. */
234
+ /** What the nested rows hang under: the toggle is named `"Rows under Notes"` whether open
235
+ * or not, and `aria-expanded` says which. */
166
236
  label: string;
167
237
  onToggle: Message;
168
238
  }>;
169
239
 
170
240
  /** The glyph slot as a fold toggle, built on `@foldkit/ui`'s Button: a ring, open or closed,
171
- * `aria-expanded`, named for what it does next. */
241
+ * `aria-expanded`, named for the rows it shows. */
172
242
  export const toggleView = <Message>(config: ToggleConfig<Message>, h: HtmlBuilder<Message>): Html =>
173
243
  UiButton.view(
174
244
  {
@@ -178,7 +248,7 @@ export const toggleView = <Message>(config: ToggleConfig<Message>, h: HtmlBuilde
178
248
  [
179
249
  ...groups.button,
180
250
  h.AriaExpanded(config.isExpanded),
181
- h.AriaLabel(`${config.isExpanded ? "Fold" : "Unfold"} ${config.label}`),
251
+ h.AriaLabel(`Rows under ${config.label}`),
182
252
  h.Class("glyph"),
183
253
  ],
184
254
  [Glyph.view({ name: config.isExpanded ? "ring" : "ring-closed" }, h)],
@@ -2,6 +2,8 @@ import { Select as UiSelect } from "@foldkit/ui";
2
2
  import { Array } from "effect";
3
3
  import type { Html, HtmlBuilder } from "foldkit/html";
4
4
 
5
+ import { describedByError, errorView } from "../field/field.ts";
6
+
5
7
  // VIEW
6
8
 
7
9
  /** One choice in a `SelectField`. */
@@ -31,8 +33,10 @@ export type ViewConfig<Message> = Readonly<{
31
33
  options: ReadonlyArray<SelectOption | SelectGroup>;
32
34
  /** A sans sentence under the control. */
33
35
  help?: string;
34
- /** Red on the control's box and on its label; say why in an ErrorSummary above the form. */
35
- isInvalid?: boolean;
36
+ /** Why the value was refused: a red sentence under the control, which the control is
37
+ * described by, with red on its box and its label (`aria-invalid`). Say it again in an
38
+ * ErrorSummary above the form. */
39
+ error?: string | undefined;
36
40
  /** The form is not sent without it: screen readers hear "required" (`aria-required`). */
37
41
  isRequired?: boolean;
38
42
  }>;
@@ -56,27 +60,18 @@ const entryView =
56
60
  : optionView(h)(entry);
57
61
 
58
62
  /** A labelled select with its help line, built on `@foldkit/ui`'s `Select`: the label names
59
- * the select, the help line describes it (`aria-describedby`), `isInvalid` sets
60
- * `aria-invalid` and `isRequired` sets `aria-required`. The stylesheet draws the box, the
61
- * caret and the picker. */
63
+ * the select, the help line describes it (`aria-describedby`), an `error` sets
64
+ * `aria-invalid` and describes it before the help, and `isRequired` sets `aria-required`.
65
+ * The stylesheet draws the box, the caret and the picker. */
62
66
  export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Message>): Html => {
63
- const {
64
- id,
65
- label,
66
- value,
67
- onChange,
68
- options,
69
- help,
70
- isInvalid = false,
71
- isRequired = false,
72
- } = config;
67
+ const { id, label, value, onChange, options, help, error, isRequired = false } = config;
73
68
 
74
69
  return UiSelect.view(
75
70
  {
76
71
  id,
77
72
  value,
78
73
  onChange,
79
- isInvalid,
74
+ isInvalid: error !== undefined,
80
75
  hasDescription: help !== undefined,
81
76
  toView: (groups) =>
82
77
  h.div(
@@ -85,9 +80,14 @@ export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Messag
85
80
  h.label(groups.label, [label]),
86
81
  // NOTE: @foldkit/ui gap: Select has no required option, so the wrapper says it.
87
82
  h.select(
88
- [...groups.select, ...(isRequired ? [h.AriaRequired(true)] : [])],
83
+ [
84
+ ...groups.select,
85
+ ...describedByError(id, error, help !== undefined, h),
86
+ ...(isRequired ? [h.AriaRequired(true)] : []),
87
+ ],
89
88
  Array.map(options, entryView(h)),
90
89
  ),
90
+ ...errorView(id, error, h),
91
91
  ...(help === undefined
92
92
  ? []
93
93
  : [h.p([...groups.description, h.Class("field-help")], [help])]),
@@ -16,8 +16,9 @@ export type ViewConfig = Readonly<{
16
16
  body: ReadonlyArray<Html | string>;
17
17
  }>;
18
18
 
19
- /** An authored page inside a panel, in an `<article>` under its `h1`: the one place the
20
- * system slows to reading pace. */
19
+ /** An authored page, an `<article>` under its `h1` on its own sheet of paper, framed as a
20
+ * panel is: the one place the system slows to reading pace. It stands in a panel's place,
21
+ * never inside one, so its `h1` opens the page instead of following a panel's `h2`. */
21
22
  export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html => {
22
23
  const { title, byline = [], actions = [], body } = config;
23
24
 
package/src/slot/slot.ts CHANGED
@@ -1,6 +1,8 @@
1
1
  import { Array } from "effect";
2
2
  import type { Html, HtmlBuilder } from "foldkit/html";
3
3
 
4
+ import type { Attributes } from "../attributes/attributes.ts";
5
+
4
6
  /** The elements a slot is wrapped in. */
5
7
  type SlotTag = "div" | "span" | "p" | "dd" | "ul";
6
8
 
@@ -11,8 +13,9 @@ export const slotView = <Message>(
11
13
  slotClass: string,
12
14
  slot: ReadonlyArray<Html | string>,
13
15
  h: HtmlBuilder<Message>,
16
+ attributes: Attributes<Message> = [],
14
17
  ): ReadonlyArray<Html> =>
15
18
  Array.match(slot, {
16
19
  onEmpty: () => [],
17
- onNonEmpty: (content) => [h[tag]([h.Class(slotClass)], content)],
20
+ onNonEmpty: (content) => [h[tag]([...attributes, h.Class(slotClass)], content)],
18
21
  });
@@ -1,5 +1,7 @@
1
1
  import { Textarea as UiTextarea } from "@foldkit/ui";
2
- import type { Html, HtmlBuilder } from "foldkit/html";
2
+ import type { ChildAttribute, Html, HtmlBuilder, TextareaAttribute } from "foldkit/html";
3
+
4
+ import { describedByError, errorView } from "../field/field.ts";
3
5
 
4
6
  // VIEW
5
7
 
@@ -11,32 +13,60 @@ export type ViewConfig<Message> = Readonly<{
11
13
  onInput: (value: string) => Message;
12
14
  /** A sans sentence under the control. */
13
15
  help?: string;
14
- /** Red on the control's rule and on its label; say why in an ErrorSummary above the form. */
15
- isInvalid?: boolean;
16
+ /** Why the value was refused: a red sentence under the control, which the control is
17
+ * described by, with red on its rule and its label (`aria-invalid`). Say it again in an
18
+ * ErrorSummary above the form. */
19
+ error?: string | undefined;
20
+ /** The form is not sent without it: screen readers hear "required" (`aria-required`). */
21
+ isRequired?: boolean;
22
+ /** Code and identifiers: the system monospace. */
23
+ isMono?: boolean;
16
24
  /** The lines it opens at; it grows with its text from there. */
17
25
  rows?: number;
26
+ /** Further attributes for the `<textarea>`, such as a placeholder or spelling off; never a
27
+ * class, and never its text, which is `value`. */
28
+ textareaAttributes?: ReadonlyArray<TextareaAttribute<Message> | ChildAttribute>;
18
29
  }>;
19
30
 
20
31
  /** A labelled textarea with its help line, built on `@foldkit/ui`'s `Textarea`: the label
21
- * names the textarea, the help line describes it (`aria-describedby`) and `isInvalid` sets
22
- * `aria-invalid`. Boxless like a Field, on a dotted baseline, with an ink corner grip. */
32
+ * names the textarea, the help line describes it (`aria-describedby`), an `error` sets
33
+ * `aria-invalid` and describes it before the help, and `isRequired` sets `aria-required`.
34
+ * Boxless like a Field, on a dotted baseline, with an ink corner grip. */
23
35
  export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Message>): Html => {
24
- const { id, label, value, onInput, help, isInvalid = false, rows } = config;
36
+ const {
37
+ id,
38
+ label,
39
+ value,
40
+ onInput,
41
+ help,
42
+ error,
43
+ isRequired = false,
44
+ isMono = false,
45
+ rows,
46
+ textareaAttributes = [],
47
+ } = config;
25
48
 
26
49
  return UiTextarea.view(
27
50
  {
28
51
  id,
29
52
  value,
30
53
  onInput,
31
- isInvalid,
54
+ isInvalid: error !== undefined,
32
55
  hasDescription: help !== undefined,
33
56
  ...(rows !== undefined && { rows }),
34
57
  toView: (groups) =>
35
58
  h.div(
36
- [h.Class("field")],
59
+ [h.Class(`field${isMono ? " mono" : ""}`)],
37
60
  [
38
61
  h.label(groups.label, [label]),
39
- h.textarea(groups.textarea),
62
+ // NOTE: @foldkit/ui gap: Textarea has no required option, so the wrapper says it.
63
+ h.textarea([
64
+ ...textareaAttributes,
65
+ ...groups.textarea,
66
+ ...describedByError(id, error, help !== undefined, h),
67
+ ...(isRequired ? [h.AriaRequired(true)] : []),
68
+ ]),
69
+ ...errorView(id, error, h),
40
70
  ...(help === undefined
41
71
  ? []
42
72
  : [h.p([...groups.description, h.Class("field-help")], [help])]),