@gjermundgaraba-designs/pixel-paper 0.11.0 → 0.12.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 (58) hide show
  1. package/DESIGN.md +11 -11
  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/radioChoices/radioChoices.d.ts +4 -4
  24. package/dist/src/radioChoices/radioChoices.d.ts.map +1 -1
  25. package/dist/src/rail/rail.d.ts +2 -2
  26. package/dist/src/rail/rail.d.ts.map +1 -1
  27. package/dist/src/rail/rail.js +5 -3
  28. package/dist/src/rows/rows.d.ts +7 -2
  29. package/dist/src/rows/rows.d.ts.map +1 -1
  30. package/dist/src/rows/rows.js +3 -3
  31. package/dist/src/selectField/selectField.d.ts +7 -5
  32. package/dist/src/selectField/selectField.d.ts.map +1 -1
  33. package/dist/src/selectField/selectField.js +12 -6
  34. package/dist/src/sheet/sheet.d.ts +3 -2
  35. package/dist/src/sheet/sheet.d.ts.map +1 -1
  36. package/dist/src/sheet/sheet.js +3 -2
  37. package/dist/src/slot/slot.d.ts +2 -1
  38. package/dist/src/slot/slot.d.ts.map +1 -1
  39. package/dist/src/slot/slot.js +2 -2
  40. package/dist/src/textareaField/textareaField.d.ts +15 -5
  41. package/dist/src/textareaField/textareaField.d.ts.map +1 -1
  42. package/dist/src/textareaField/textareaField.js +15 -6
  43. package/package.json +1 -1
  44. package/src/emptyState/emptyState.ts +8 -2
  45. package/src/field/field.ts +37 -8
  46. package/src/fieldset/fieldset.ts +16 -4
  47. package/src/lineDiff/diff.ts +104 -48
  48. package/src/lineDiff/index.ts +2 -2
  49. package/src/lineDiff/lineDiff.ts +76 -31
  50. package/src/passwordField/passwordField.ts +18 -9
  51. package/src/pixel-paper.css +26 -5
  52. package/src/radioChoices/radioChoices.ts +2 -2
  53. package/src/rail/rail.ts +5 -3
  54. package/src/rows/rows.ts +9 -4
  55. package/src/selectField/selectField.ts +17 -17
  56. package/src/sheet/sheet.ts +3 -2
  57. package/src/slot/slot.ts +4 -1
  58. package/src/textareaField/textareaField.ts +39 -9
@@ -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/rows.ts CHANGED
@@ -55,6 +55,10 @@ export type RowConfig<Message> = Readonly<{
55
55
  body: ReadonlyArray<Html | string>;
56
56
  /** Right-aligned chips, marks, counts, due days. */
57
57
  slot?: ReadonlyArray<Html | string>;
58
+ /** Further attributes for the slot's `span.slot`, such as an id the row's line is described
59
+ * by (`aria-describedby`); there is no span, and so nothing to point at, while `slot` is
60
+ * empty. Never a class. */
61
+ slotAttributes?: Attributes<Message>;
58
62
  /** Controls that belong to the row's job, before the tools (an approve and reject pair). */
59
63
  actions?: ReadonlyArray<Html | string>;
60
64
  /** Icon buttons at the row's right end, dimmed until the row is hovered or focused (full
@@ -95,7 +99,7 @@ const lineView = <Message>(
95
99
  ...box,
96
100
  ...(config.glyph === undefined ? [] : [config.glyph]),
97
101
  text,
98
- ...slotView("span", "slot", config.slot ?? [], h),
102
+ ...slotView("span", "slot", config.slot ?? [], h, config.slotAttributes),
99
103
  ...(config.actions ?? []),
100
104
  ...slotView("span", "tools", config.tools ?? [], h),
101
105
  ],
@@ -162,13 +166,14 @@ export const glyphView = <Message>(config: GlyphConfig, h: HtmlBuilder<Message>)
162
166
  /** Configuration for rendering a fold toggle with `toggleView`. */
163
167
  export type ToggleConfig<Message> = Readonly<{
164
168
  isExpanded: boolean;
165
- /** What folds: `"Fold Notes"` and `"Unfold Notes"` are built from it. */
169
+ /** What the nested rows hang under: the toggle is named `"Rows under Notes"` whether open
170
+ * or not, and `aria-expanded` says which. */
166
171
  label: string;
167
172
  onToggle: Message;
168
173
  }>;
169
174
 
170
175
  /** 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. */
176
+ * `aria-expanded`, named for the rows it shows. */
172
177
  export const toggleView = <Message>(config: ToggleConfig<Message>, h: HtmlBuilder<Message>): Html =>
173
178
  UiButton.view(
174
179
  {
@@ -178,7 +183,7 @@ export const toggleView = <Message>(config: ToggleConfig<Message>, h: HtmlBuilde
178
183
  [
179
184
  ...groups.button,
180
185
  h.AriaExpanded(config.isExpanded),
181
- h.AriaLabel(`${config.isExpanded ? "Fold" : "Unfold"} ${config.label}`),
186
+ h.AriaLabel(`Rows under ${config.label}`),
182
187
  h.Class("glyph"),
183
188
  ],
184
189
  [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])]),