@gjermundgaraba-designs/pixel-paper 0.12.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 CHANGED
@@ -1009,7 +1009,7 @@ The printed button: a paper rectangle in a 2px ink frame that casts a 3px shadow
1009
1009
  - **Segmented** → `div.nav[role="group"] > button[aria-pressed]`: a choice of one printed as the same strip. When every option carries a `glyph` it prints as `div.switch > button[aria-pressed][aria-label]`, the strip holding 26×22px glyph cells (a layout, a density), each option's label its name and its tip. **Scheme.SchemeSwitch** is a Submodel printing a Segmented of system, paper and plum that remembers the choice (see Palette below).
1010
1010
  - **Tabs** (Submodel over `@foldkit/ui`'s Tabs; the caller keeps the tab on show) → `div.tabbed > div.nav.tabs[role="tablist"] > button[role="tab"][aria-selected]` plus `div.tab-panel[role="tabpanel"]`: the strip at 11px with `4px 12px` segments, a count spaced as NavStrip's; arrows, Home and End move and select. At the top of a flush panel the strip sits inside the panel's padding.
1011
1011
  - **Crumbs** → `nav.crumbs > ol > li`: the path in the 11px pixel face, muted slashes between, the last crumb ink with `aria-current="page"`, the others muted with a dotted underline on hover.
1012
- - **Places** (a View over `@foldkit/ui`'s Button) → `nav.places[aria-label] > ul > li`, each place (keyed by an `id` that is stable and unique among the places, so two places that share a name stay apart) → `li > button` holding `span.place-name` and `span.place-count`, then `span.place-tools`: a column of places to go (areas, collections, saved filters), flush in a rail panel. Each line is at least 34px (`--row`), `8px 12px`, over a 1px rule; the name in the sans, sentence case, wrapping anywhere rather than clipping; the count in the 11px pixel face, muted, at the right; a quiet IconButton after it acts on the place itself (remove a saved filter). Hover tints head. The place you are in carries `aria-current="page"` and its whole `li` prints inverted, the count and a quiet tool in paper at 0.75, never tinted by hover; the ring sits 4px inside the line, in paper on the current one.
1012
+ - **Places** (a View over `@foldkit/ui`'s Button) → `nav.places[aria-label] > ul > li`, each place (keyed by an `id` that is stable and unique among the places, so two places that share a name stay apart) → `li > a[href]` when the place has an address (an area, a saved view: it opens in a new tab and reads as a link) or `li > button` when choosing it only changes the page, holding `span.place-name` and `span.place-count`, then `span.place-tools`: a column of places to go (areas, collections, saved filters), flush in a rail panel. Each line is at least 34px (`--row`), `8px 12px`, over a 1px rule; the name in the sans, sentence case, wrapping anywhere rather than clipping; the count in the 11px pixel face, muted, at the right; a quiet IconButton after it acts on the place itself (remove a saved filter). Hover tints head. The place you are in carries `aria-current="page"` and its whole `li` prints inverted, the count and a quiet tool in paper at 0.75, never tinted by hover; the ring sits 4px inside the line, in paper on the current one.
1013
1013
  - **PageHead** → `header.page-head > div.page-name > h1.page-title + span.meta`, then `div.page-acts`: the heading of a working screen, one per screen, above its panels (which title in h2). The name in the page-title register with a count or state beside it in the 11px pixel face; the screen's own Buttons at the right, 8px apart, wrapping under the name when the line runs out (`14px × 24px` gaps), aligned to the name's foot. No surface of its own: it stands on the ground. There is no label above the heading; the name says where you are.
1014
1014
  - **TopBar** (a Mount measures it, reporting through `toParentMessage`) → `header.topbar` (brand, nav, `div.topbar-tools`), **Wordmark** → `.wordmark` with `span.cursor`, **LinkState** → `span.link-state[role="status"]`: online prints plain; `.connecting` (the first reach) prints muted with the working cursor; `.reconnecting` (the link dropped and is coming back) fills amber with `on-status` and the cursor; `.offline` (given up) fills red with `on-red`. Reconnecting and offline stay in a phone's top bar; the other two hide there.
1015
1015
  - **StatusBar** (a Mount measures it, reporting through `toParentMessage`) → `footer.statusbar > div.statusbar-state + p.statusbar-keys`: the quiet line along the bottom of an interface, the page's footer (one per page, after the rail and the deck, outside `main`). Fixed to the bottom edge on paper over a 1px rule, in the pixel face at the state size in Muted Ink: the status at the left (a LinkState, a sync word), the keys at the right, each a `kbd.key-hint` in ink (printed as the help prints it, ⌘K or Ctrl+K by platform) and its word, plain text and not pressable. Narrow, the keys wrap under the status; nothing hides. It keeps `--statusbar-h` on the page at its height (25px, one line, until it is measured), like the top bar's `--topbar-h`: the page pads its end by it, a scrolled-to anchor stays clear of it, and the rail, the stage, a Banner and the toast stack all stop above it.
@@ -1068,7 +1068,7 @@ One keyboard for the page, Vim first, with the arrows beside the letters. The ke
1068
1068
 
1069
1069
  - **KeyHelp** (Submodel; `KeyHelp.subscriptions` bind `?`) → a Dialog holding a narrow Card titled "Keys": the application's own words when it gives `toIntroView` (what the covered page would say that the reader still needs, a sync notice and its retry), a text Field ("Find a key", focused on open) that finds a key by its key or its words, then a Keymap of every binding given, under its group, each key listed once under the command it gives, and a note saying that keys wait while you type in a field and, when sequences exist, that a sequence's keys are typed one straight after the other and any other key lets go of the first. That is what Foldkit's key bindings do; any other pause (while a card is held, while a menu is open) is the application's to make, so the note does not claim it. `?` opens it and so does `KeyHelp.open` from the parent's update (a button, a palette entry). Render it once per page, with the bindings in force on that page. `Closed` reports the reader closing it (Escape, the veil, a close button); opening it reports nothing, by `?` or by `open`, so read `KeyHelp.isOpen` to keep another dialog from opening over the help.
1070
1070
  - **KeyHint** → `kbd.key-hint[aria-hidden="true"]` (Button's and a menu item's `keyHint`), a key sequence printed as the help prints it: arrows as arrows, a sequence of characters as one word (`["G", "G"]` prints gg; named keys keep their space), `Mod+K` as ⌘K or Ctrl+K, Shift as ⇧ or Shift+. The platform lives in the application's Model as `KeyHint.KeyPlatform`, `"Other"` until `KeyHint.detectKeyPlatform` runs once from `init`, so a server render prints Ctrl. The same notation is exported as `KeyHint.keyLabel(keys, platform)`, for anything else that names a key (a command palette's hint).
1071
- - **A list's cursor** is the application's: it keeps the current item in its Model, sets `aria-current="true"` and a roving `tabindex` (0 on the current item, -1 elsewhere) on that row's `div.row` or on that board card (`Board.cardView`'s `attributes`), and binds j and k (and ↓ ↑) to move it, `g g` and G (Home and End) to go to the ends, in an outline h and l to fold and unfold or step to the parent and the first child, on a board h and l to cross columns, and Enter to open, with the commands on the current item (`d d` removes it) beside them. A move focuses the item and scrolls it into view by the least distance; when the current item leaves (filtered, removed, folded away) the cursor takes its place. While a Board holds a card, the arrows and Space are the move's: turn the page's own bindings for them off. Give a Row `role="group"` and a label.
1071
+ - **A list's cursor** is the application's: it keeps the current item in its Model, sets `aria-current="true"` and a roving `tabindex` (0 on the current item, -1 elsewhere) on that row's `div.row` or on that board card (`Board.cardView`'s `attributes`), and binds j and k (and ↓ ↑) to move it, `g g` and G (Home and End) to go to the ends, in an outline h and l to fold and unfold or step to the parent and the first child, on a board h and l to cross columns, and Enter to open, with the commands on the current item (`d d` removes it) beside them. A move focuses the item and scrolls it into view by the least distance; when the current item leaves (filtered, removed, folded away) the cursor takes its place. While a Board holds a card, the arrows and Space are the move's: turn the page's own bindings for them off. A keyboard-driven list of Rows is a treegrid (`Rows.view`'s `isTreegrid`, named by its `label`), so a screen reader hands the list its arrows and letters instead of taking them for browsing: each row carries its place (`treegrid`: its level, its position among its siblings, how many there are, and whether it is expanded when it folds) and a name in its `attributes`.
1072
1072
 
1073
1073
  ### Tips and toolbars
1074
1074
 
@@ -1084,7 +1084,7 @@ One keyboard for the page, Vim first, with the arrows beside the letters. The ke
1084
1084
 
1085
1085
  ### Rows and the outline
1086
1086
 
1087
- **Rows** → `ul.rows` (`.tree` for an outline); each row, `Rows.rowView` → `li > div.row` (`.context`) with an optional nested `ul.rows`. A context row (outside a filter, printed only to keep a match on its chain) is muted and carries no chips, and since muting is colour alone it ends its text with `span.sr-only` " (context)", which a selectable row's box takes into its name. A 34px flex row (`5px 12px`, 8px gap): an 18px glyph cell (`Rows.glyphView` → `span.glyph`, or `Rows.toggleView` → `button.glyph[aria-expanded]` with the ring glyph open or closed, named "Rows under" its label whether open or not), `div.grow` holding the text (a `label.grow` when it is the label of the row's box; `Rows.titleView` → `button.title`, underlined on hover, `.done-title` when done), `span.slot` for chips, marks and due days, the row's actions, and `span.tools` (`tools`, icon buttons) resting at 55% opacity until hover or focus-within (hidden on phones). Hover and focus-within tint the row head with `background-color` only, so the indent guides survive; the cursor row takes neither. A keyboard-driven list puts its cursor on `div.row` (`aria-current="true"`, a roving `tabindex`), which the stylesheet outlines (see "The cursor" below), through `Rows.rowView`'s `attributes`, which go on `div.row` rather than its `li` and also carry the row's `role="group"`, its name and its key handlers (never a class). Nested lists indent 26px on a 1px rule guide. **InlineInput** (Submodel; its `init` focuses and selects the field, so fold it in with `Update.foldChildInit`) → `input.inline-input` swaps in for text at exactly `1lh + 1px` so the row never moves. Enter or leaving the field reports new, non-empty text, trimmed, as `Committed`; an untouched field reports nothing, so a remote edit is never written back. The field then holds `readonly` and `aria-busy` (muted, still focused) until the parent answers with `InlineInput.succeeded` or `InlineInput.failed`: a failure keeps the field and its text for another try and, when it is a refusal with a reason, says why through `InlineInput.errorView`, which the field points at (`aria-invalid`, a red rule, and `aria-describedby`); the parent reads the held reason with `InlineInput.maybeError`. The parent's own `error` (the item's parent was deleted while editing) marks the field the same way, given to the view and to `errorView` alike, and shows before a refused save's reason. `Ended` says the edit is over (`"Enter"`, `"Blur"` or `"Escape"`, and `"Blur"` when focus left while the save was out) with the id the edit's last save returned (`maybeLastSavedId`, the item a repeat added last); the parent moves focus on `"Enter"` and `"Escape"` only, so a clicked destination keeps it. `isRepeating` (adding several) empties the field after each saved Enter instead of ending; Enter on an empty one ends. The text lives in the Submodel's Model, so it outlives a render. Escape is stopped from closing a dialog around it; an IME's Enter and Escape stay with the IME; removing the field writes nothing. **EditableTitle** (Submodel; it reports `Committed`, which the parent answers with `EditableTitle.succeeded` or `failed`) → `button.title-display` ⇄ `input.inline-input.title-edit` at 16px/600; Enter and Escape put focus back on the title.
1087
+ **Rows** → `ul.rows` (`.tree` for an outline); each row, `Rows.rowView` → `li > div.row` (`.context`) with an optional nested `ul.rows`. A context row (outside a filter, printed only to keep a match on its chain) is muted and carries no chips, and since muting is colour alone it ends its text with `span.sr-only` " (context)", which a selectable row's box takes into its name. A 34px flex row (`5px 12px`, 8px gap): an 18px glyph cell (`Rows.glyphView` → `span.glyph`, or `Rows.toggleView` → `button.glyph[aria-expanded]` with the ring glyph open or closed, named "Rows under" its label whether open or not), `div.grow` holding the text (a `label.grow` when it is the label of the row's box; `Rows.titleView` → `button.title`, underlined on hover, `.done-title` when done), `span.slot` for chips, marks and due days, the row's actions, and `span.tools` (`tools`, icon buttons) resting at 55% opacity until hover or focus-within (hidden on phones). Hover and focus-within tint the row head with `background-color` only, so the indent guides survive; the cursor row takes neither. A keyboard-driven list is a treegrid: `ul.rows[role="treegrid"]` named by its label, each `li` and nested `ul` stepping aside (`role="none"`), each `div.row[role="row"]` carrying `aria-level`, `aria-posinset`, `aria-setsize` and, on a row that folds, `aria-expanded`, and each of its parts a `role="gridcell"` (the box's `span.row-check`, the text, `span.slot`, `span.tools`, and `span.cell` around the glyph and around the actions, laid out as the row lays them out). It puts its cursor on `div.row` (`aria-current="true"`, a roving `tabindex`), which the stylesheet outlines (see "The cursor" below), through `Rows.rowView`'s `attributes`, which go on `div.row` rather than its `li` and also carry the row's name and its key handlers (never a class). A list no keyboard drives stays `ul > li`; a row there named for a screen reader takes `role="group"` with its name. Nested lists indent 26px on a 1px rule guide. **InlineInput** (Submodel; its `init` focuses and selects the field, so fold it in with `Update.foldChildInit`) → `input.inline-input` swaps in for text at exactly `1lh + 1px` so the row never moves. Enter or leaving the field reports new, non-empty text, trimmed, as `Committed`; an untouched field reports nothing, so a remote edit is never written back. The field then holds `readonly` and `aria-busy` (muted, still focused) until the parent answers with `InlineInput.succeeded` or `InlineInput.failed`: a failure keeps the field and its text for another try and, when it is a refusal with a reason, says why through `InlineInput.errorView`, which the field points at (`aria-invalid`, a red rule, and `aria-describedby`); the parent reads the held reason with `InlineInput.maybeError`. The parent's own `error` (the item's parent was deleted while editing) marks the field the same way, given to the view and to `errorView` alike, and shows before a refused save's reason. `Ended` says the edit is over (`"Enter"`, `"Blur"` or `"Escape"`, and `"Blur"` when focus left while the save was out) with the id the edit's last save returned (`maybeLastSavedId`, the item a repeat added last); the parent moves focus on `"Enter"` and `"Escape"` only, so a clicked destination keeps it. `isRepeating` (adding several) empties the field after each saved Enter instead of ending; Enter on an empty one ends. The text lives in the Submodel's Model, so it outlives a render. Escape is stopped from closing a dialog around it; an IME's Enter and Escape stay with the IME; removing the field writes nothing. **EditableTitle** (Submodel; it reports `Committed`, which the parent answers with `EditableTitle.succeeded` or `failed`) → `button.title-display` ⇄ `input.inline-input.title-edit` at 16px/600; Enter and Escape put focus back on the title.
1088
1088
 
1089
1089
  **The cursor:** `div.row[aria-current="true"]`, set on the row by the application, the one row the keyboard's commands act on. It is drawn, not filled: a 1px Muted Ink outline 4px inside the row (`outline-offset: -4px`), so a nested row's follows its indent and none leaves the panel. It takes no fill at all, not even the hover or focus tint other rows take, and its text, chips and tools keep their own colours (the tools at full opacity). Focus on the row itself thickens the same line to 2px (`--line`): one line inside the row, never a second ring outside it, for any focused row. It is neither focus nor selection (the check cell), it stays put when focus leaves, and moving it is the application's (see Keys). Controls in it keep their own ring: the text cell (`.grow`) clips, but with 4px of room inside the clip, so a title's ring prints whole; an InlineInput in a row shows its focus as its caret and its rule doubled to 2px (drawn inside its box, so the row keeps its height; red while refused), not a ring. A board's cursor card is inverted instead (see Board).
1090
1090
 
@@ -1,6 +1,8 @@
1
1
  import type { Html, HtmlBuilder } from "foldkit/html";
2
- /** One place in Places: a button that shows it. */
3
- export type Place<Message> = Readonly<{
2
+ /** One place in Places: a link to its address (`href`), or a button that shows it in the page
3
+ * (`onSelect`). */
4
+ export type Place<Message> = PlaceContent & PlaceTarget<Message>;
5
+ type PlaceContent = Readonly<{
4
6
  /** Stable and unique among the places: the row's key, so two places that share a name stay
5
7
  * apart. */
6
8
  id: string;
@@ -12,8 +14,15 @@ export type Place<Message> = Readonly<{
12
14
  count?: number;
13
15
  /** A quiet IconButton after it that acts on the place itself (remove a saved filter). */
14
16
  tools?: ReadonlyArray<Html>;
15
- /** Shows the place. */
17
+ }>;
18
+ type PlaceTarget<Message> = Readonly<{
19
+ /** The place's address: the line is a link, so it opens in a new tab and reads as one. */
20
+ href: string;
21
+ onSelect?: never;
22
+ }> | Readonly<{
23
+ /** Shows the place without leaving the page: the line is a button. */
16
24
  onSelect: Message;
25
+ href?: never;
17
26
  }>;
18
27
  /** Configuration for rendering places with `view`. */
19
28
  export type ViewConfig<Message> = Readonly<{
@@ -22,7 +31,8 @@ export type ViewConfig<Message> = Readonly<{
22
31
  places: ReadonlyArray<Place<Message>>;
23
32
  }>;
24
33
  /** A column of places to go (areas, collections, saved filters), flush in a rail panel: a
25
- * navigation landmark named by its label, one line each a button built on `@foldkit/ui`'s
26
- * Button, the one you are in `aria-current="page"` and printed inverted. */
34
+ * navigation landmark named by its label, one line each a link to the place or a button built
35
+ * on `@foldkit/ui`'s Button, the one you are in `aria-current="page"` and printed inverted. */
27
36
  export declare const view: <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Message>) => Html;
37
+ export {};
28
38
  //# sourceMappingURL=places.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"places.d.ts","sourceRoot":"","sources":["../../../src/places/places.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAOtD,mDAAmD;AACnD,MAAM,MAAM,KAAK,CAAC,OAAO,IAAI,QAAQ,CAAC;IACpC;iBACa;IACb,EAAE,EAAE,MAAM,CAAC;IACX,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yFAAyF;IACzF,KAAK,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC;IAC5B,uBAAuB;IACvB,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC,CAAC;AAEH,sDAAsD;AACtD,MAAM,MAAM,UAAU,CAAC,OAAO,IAAI,QAAQ,CAAC;IACzC,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,aAAa,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;CACvC,CAAC,CAAC;AAgCH;;6EAE6E;AAC7E,eAAO,MAAM,IAAI,GAAI,OAAO,UAAU,UAAU,CAAC,OAAO,CAAC,KAAK,WAAW,CAAC,OAAO,CAAC,KAAG,IAOpF,CAAC"}
1
+ {"version":3,"file":"places.d.ts","sourceRoot":"","sources":["../../../src/places/places.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAOtD;oBACoB;AACpB,MAAM,MAAM,KAAK,CAAC,OAAO,IAAI,YAAY,GAAG,WAAW,CAAC,OAAO,CAAC,CAAC;AAEjE,KAAK,YAAY,GAAG,QAAQ,CAAC;IAC3B;iBACa;IACb,EAAE,EAAE,MAAM,CAAC;IACX,gFAAgF;IAChF,KAAK,EAAE,MAAM,CAAC;IACd,qEAAqE;IACrE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,gEAAgE;IAChE,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,yFAAyF;IACzF,KAAK,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC;CAC7B,CAAC,CAAC;AAEH,KAAK,WAAW,CAAC,OAAO,IACpB,QAAQ,CAAC;IACP,0FAA0F;IAC1F,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,CAAC,EAAE,KAAK,CAAC;CAClB,CAAC,GACF,QAAQ,CAAC;IACP,sEAAsE;IACtE,QAAQ,EAAE,OAAO,CAAC;IAClB,IAAI,CAAC,EAAE,KAAK,CAAC;CACd,CAAC,CAAC;AAEP,sDAAsD;AACtD,MAAM,MAAM,UAAU,CAAC,OAAO,IAAI,QAAQ,CAAC;IACzC,yDAAyD;IACzD,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,aAAa,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC;CACvC,CAAC,CAAC;AAgCH;;gGAEgG;AAChG,eAAO,MAAM,IAAI,GAAI,OAAO,UAAU,UAAU,CAAC,OAAO,CAAC,KAAK,WAAW,CAAC,OAAO,CAAC,KAAG,IAOpF,CAAC"}
@@ -3,24 +3,26 @@ import { Button as UiButton } from "@foldkit/ui";
3
3
  import { Array } from "effect";
4
4
  import { LOCALE } from "../locale/locale.js";
5
5
  import { slotView } from "../slot/slot.js";
6
- const placeView = (h) => __foldkitBrandViewResult(((place) => {
7
- const { label, isCurrent = false, count, tools = [], onSelect } = place;
8
- return __foldkitBrandViewResult((h.keyed("li")(place.id, [], [
9
- UiButton.view({
10
- onClick: onSelect,
11
- toView: (groups) => __foldkitBrandViewResult((h.button([...groups.button, ...(isCurrent ? [h.AriaCurrent("page")] : [])], [
12
- h.span([h.Class("place-name")], [label]),
13
- ...(count === undefined
14
- ? []
15
- : [" ", h.span([h.Class("place-count")], [count.toLocaleString(LOCALE)])]),
16
- ])), "@gjermundgaraba-designs/pixel-paper/src/places/places.js#toView"),
17
- }, h),
18
- ...slotView("span", "place-tools", tools, h),
19
- ])), "@gjermundgaraba-designs/pixel-paper/src/places/places.js#anonymous");
20
- }), "@gjermundgaraba-designs/pixel-paper/src/places/places.js#placeView");
6
+ const lineView = (place, h) => {
7
+ const { label, isCurrent = false, count } = place;
8
+ const current = isCurrent ? [h.AriaCurrent("page")] : [];
9
+ const content = [
10
+ h.span([h.Class("place-name")], [label]),
11
+ ...(count === undefined
12
+ ? []
13
+ : [" ", h.span([h.Class("place-count")], [count.toLocaleString(LOCALE)])]),
14
+ ];
15
+ return __foldkitBrandViewResult((place.href === undefined
16
+ ? UiButton.view({
17
+ onClick: place.onSelect,
18
+ toView: (groups) => __foldkitBrandViewResult((h.button([...groups.button, ...current], content)), "@gjermundgaraba-designs/pixel-paper/src/places/places.js#toView"),
19
+ }, h)
20
+ : h.a([h.Href(place.href), ...current], content)), "@gjermundgaraba-designs/pixel-paper/src/places/places.js#lineView");
21
+ };
22
+ const placeView = (h) => __foldkitBrandViewResult(((place) => __foldkitBrandViewResult((h.keyed("li")(place.id, [], [lineView(place, h), ...slotView("span", "place-tools", place.tools ?? [], h)])), "@gjermundgaraba-designs/pixel-paper/src/places/places.js#anonymous")), "@gjermundgaraba-designs/pixel-paper/src/places/places.js#placeView");
21
23
  /** A column of places to go (areas, collections, saved filters), flush in a rail panel: a
22
- * navigation landmark named by its label, one line each a button built on `@foldkit/ui`'s
23
- * Button, the one you are in `aria-current="page"` and printed inverted. */
24
+ * navigation landmark named by its label, one line each a link to the place or a button built
25
+ * on `@foldkit/ui`'s Button, the one you are in `aria-current="page"` and printed inverted. */
24
26
  export const view = (config, h) => {
25
27
  const { label, places } = config;
26
28
  return __foldkitBrandViewResult((h.nav([h.AriaLabel(label), h.Class("places")], [h.ul([], Array.map(places, placeView(h)))])), "@gjermundgaraba-designs/pixel-paper/src/places/places.js#view");
@@ -1,3 +1,3 @@
1
1
  export { glyphView, rowView, titleView, toggleView, view } from "./rows.ts";
2
- export type { GlyphConfig, RowConfig, RowSelect, TitleConfig, ToggleConfig, ViewConfig, } from "./rows.ts";
2
+ export type { GlyphConfig, RowConfig, RowSelect, TitleConfig, ToggleConfig, TreegridPlace, ViewConfig, } from "./rows.ts";
3
3
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/rows/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC5E,YAAY,EACV,WAAW,EACX,SAAS,EACT,SAAS,EACT,WAAW,EACX,YAAY,EACZ,UAAU,GACX,MAAM,WAAW,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/rows/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,OAAO,EAAE,SAAS,EAAE,UAAU,EAAE,IAAI,EAAE,MAAM,WAAW,CAAC;AAC5E,YAAY,EACV,WAAW,EACX,SAAS,EACT,SAAS,EACT,WAAW,EACX,YAAY,EACZ,aAAa,EACb,UAAU,GACX,MAAM,WAAW,CAAC"}
@@ -7,8 +7,12 @@ export type ViewConfig = Readonly<{
7
7
  rows: ReadonlyArray<Html>;
8
8
  /** An outline: nested rows indent 26px and hang on a 1px rule guide. */
9
9
  isOutline?: boolean;
10
- /** Names the list for screen readers. */
10
+ /** Names the list for screen readers; a treegrid needs it. */
11
11
  label?: string;
12
+ /** A keyboard-driven list, whose cursor the application moves (see Keys in DESIGN.md): a
13
+ * `role="treegrid"`, so a screen reader hands it the arrow and letter keys. Give every row
14
+ * its `treegrid` place. */
15
+ isTreegrid?: boolean;
12
16
  }>;
13
17
  /** A list of 34px rows. */
14
18
  export declare const view: <Message>(config: ViewConfig, h: HtmlBuilder<Message>) => Html;
@@ -26,6 +30,19 @@ export type RowSelect<Message> = Readonly<{
26
30
  * toggles it wherever it is clicked, on the button or the link too. */
27
31
  label?: string;
28
32
  }>;
33
+ /** A row's place in a treegrid (`view`'s `isTreegrid`): its line is a `role="row"` and each
34
+ * of its parts a `role="gridcell"`, while the list items around them step aside
35
+ * (`role="none"`). */
36
+ export type TreegridPlace = Readonly<{
37
+ /** Its depth, from 1 at the list's roots: `aria-level`. */
38
+ level: number;
39
+ /** Its place among its siblings, from 1: `aria-posinset`. */
40
+ position: number;
41
+ /** How many siblings it has, itself included: `aria-setsize`. */
42
+ setSize: number;
43
+ /** Whether the rows under it show, for a row that folds: `aria-expanded`. */
44
+ isExpanded?: boolean;
45
+ }>;
29
46
  /** Configuration for rendering a row with `rowView`. */
30
47
  export type RowConfig<Message> = Readonly<{
31
48
  /** The row's stable id among its siblings. */
@@ -53,14 +70,18 @@ export type RowConfig<Message> = Readonly<{
53
70
  isContext?: boolean;
54
71
  /** Makes the row selectable: a box at its left edge. */
55
72
  select?: RowSelect<Message>;
73
+ /** The row's place in a treegrid; every row of a treegrid has one. */
74
+ treegrid?: TreegridPlace;
56
75
  /** Further attributes for the row's line, the `div.row`, not its `li`: a keyboard list's
57
76
  * cursor (`aria-current="true"`, which the stylesheet outlines, and a roving `tabindex`),
58
- * `role="group"` and a name, data attributes, key and focus handlers. Never a class. */
77
+ * its name (with `role="group"` outside a treegrid, whose row is already a named `row`), data
78
+ * attributes, key and focus handlers. Never a class. */
59
79
  attributes?: Attributes<Message>;
60
80
  }>;
61
81
  /** One 34px row: box, glyph, text, slot, actions, tools, and the rows nested under it. Hover
62
82
  * and focus tint it head. The box of a selectable row is built on `@foldkit/ui`'s Checkbox:
63
- * a `role="checkbox"` button (`aria-checked`) whose whole cell is its target. */
83
+ * a `role="checkbox"` button (`aria-checked`) whose whole cell is its target. In a treegrid
84
+ * the line is the `role="row"` and each part a `role="gridcell"`. */
64
85
  export declare const rowView: <Message>(config: RowConfig<Message>, h: HtmlBuilder<Message>) => Html;
65
86
  /** Configuration for rendering a glyph slot with `glyphView`. */
66
87
  export type GlyphConfig = Readonly<{
@@ -1 +1 @@
1
- {"version":3,"file":"rows.d.ts","sourceRoot":"","sources":["../../../src/rows/rows.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"rows.d.ts","sourceRoot":"","sources":["../../../src/rows/rows.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAEtD,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAE9D,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAKpD,8DAA8D;AAC9D,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,gCAAgC;IAChC,IAAI,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC;IAC1B,wEAAwE;IACxE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,8DAA8D;IAC9D,KAAK,CAAC,EAAE,MAAM,CAAC;IACf;;gCAE4B;IAC5B,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC,CAAC;AAEH,2BAA2B;AAC3B,eAAO,MAAM,IAAI,GAAI,OAAO,UAAU,UAAU,KAAK,WAAW,CAAC,OAAO,CAAC,KAAG,IAQzE,CAAC;AAIJ;;+FAE+F;AAC/F,MAAM,MAAM,SAAS,CAAC,OAAO,IAAI,QAAQ,CAAC;IACxC;cACU;IACV,EAAE,EAAE,MAAM,CAAC;IACX,SAAS,EAAE,OAAO,CAAC;IACnB,4BAA4B;IAC5B,QAAQ,EAAE,CAAC,SAAS,EAAE,OAAO,KAAK,OAAO,CAAC;IAC1C;4EACwE;IACxE,KAAK,CAAC,EAAE,MAAM,CAAC;CAChB,CAAC,CAAC;AAEH;;uBAEuB;AACvB,MAAM,MAAM,aAAa,GAAG,QAAQ,CAAC;IACnC,2DAA2D;IAC3D,KAAK,EAAE,MAAM,CAAC;IACd,6DAA6D;IAC7D,QAAQ,EAAE,MAAM,CAAC;IACjB,iEAAiE;IACjE,OAAO,EAAE,MAAM,CAAC;IAChB,6EAA6E;IAC7E,UAAU,CAAC,EAAE,OAAO,CAAC;CACtB,CAAC,CAAC;AAEH,wDAAwD;AACxD,MAAM,MAAM,SAAS,CAAC,OAAO,IAAI,QAAQ,CAAC;IACxC,8CAA8C;IAC9C,GAAG,EAAE,MAAM,CAAC;IACZ,yEAAyE;IACzE,KAAK,CAAC,EAAE,IAAI,CAAC;IACb,8DAA8D;IAC9D,IAAI,EAAE,aAAa,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;IACnC,oDAAoD;IACpD,IAAI,CAAC,EAAE,aAAa,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;IACpC;;gCAE4B;IAC5B,cAAc,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;IACrC,4FAA4F;IAC5F,OAAO,CAAC,EAAE,aAAa,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;IACvC;kEAC8D;IAC9D,KAAK,CAAC,EAAE,aAAa,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;IACrC,oEAAoE;IACpE,MAAM,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC;IAC7B;;qEAEiE;IACjE,SAAS,CAAC,EAAE,OAAO,CAAC;IACpB,wDAAwD;IACxD,MAAM,CAAC,EAAE,SAAS,CAAC,OAAO,CAAC,CAAC;IAC5B,sEAAsE;IACtE,QAAQ,CAAC,EAAE,aAAa,CAAC;IACzB;;;6DAGyD;IACzD,UAAU,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;CAClC,CAAC,CAAC;AAwGH;;;sEAGsE;AACtE,eAAO,MAAM,OAAO,GAAI,OAAO,UAAU,SAAS,CAAC,OAAO,CAAC,KAAK,WAAW,CAAC,OAAO,CAAC,KAAG,IAStF,CAAC;AAIF,iEAAiE;AACjE,MAAM,MAAM,WAAW,GAAG,QAAQ,CAAC;IACjC,IAAI,EAAE,SAAS,CAAC;CACjB,CAAC,CAAC;AAEH,sCAAsC;AACtC,eAAO,MAAM,SAAS,GAAI,OAAO,UAAU,WAAW,KAAK,WAAW,CAAC,OAAO,CAAC,KAAG,IACd,CAAC;AAErE,mEAAmE;AACnE,MAAM,MAAM,YAAY,CAAC,OAAO,IAAI,QAAQ,CAAC;IAC3C,UAAU,EAAE,OAAO,CAAC;IACpB;kDAC8C;IAC9C,KAAK,EAAE,MAAM,CAAC;IACd,QAAQ,EAAE,OAAO,CAAC;CACnB,CAAC,CAAC;AAEH;oDACoD;AACpD,eAAO,MAAM,UAAU,GAAI,OAAO,UAAU,YAAY,CAAC,OAAO,CAAC,KAAK,WAAW,CAAC,OAAO,CAAC,KAAG,IAgB1F,CAAC;AAIJ,kEAAkE;AAClE,MAAM,MAAM,WAAW,CAAC,OAAO,IAAI,QAAQ,CAAC;IAC1C,KAAK,EAAE,MAAM,CAAC;IACd,MAAM,EAAE,OAAO,CAAC;IAChB,mDAAmD;IACnD,MAAM,CAAC,EAAE,OAAO,CAAC;CAClB,CAAC,CAAC;AAEH;2BAC2B;AAC3B,eAAO,MAAM,SAAS,GAAI,OAAO,UAAU,WAAW,CAAC,OAAO,CAAC,KAAK,WAAW,CAAC,OAAO,CAAC,KAAG,IAWxF,CAAC"}
@@ -4,41 +4,70 @@ import * as Glyph from "../glyph/glyph.js";
4
4
  import { slotView } from "../slot/slot.js";
5
5
  /** A list of 34px rows. */
6
6
  export const view = (config, h) => __foldkitBrandViewResult((h.ul([
7
+ ...(config.isTreegrid === true ? [h.Role("treegrid")] : []),
7
8
  ...(config.label === undefined ? [] : [h.AriaLabel(config.label)]),
8
9
  h.Class(config.isOutline === true ? "rows tree" : "rows"),
9
10
  ], config.rows)), "@gjermundgaraba-designs/pixel-paper/src/rows/rows.js#view");
11
+ const cellRole = (config, h) => (__foldkitBrandViewResult((config.treegrid === undefined ? [] : [h.Role("gridcell")]), "@gjermundgaraba-designs/pixel-paper/src/rows/rows.js#cellRole"));
12
+ const rowRole = (place, h) => __foldkitBrandViewResult((place === undefined
13
+ ? []
14
+ : [
15
+ h.Role("row"),
16
+ h.AriaLevel(place.level),
17
+ h.AriaPosinset(place.position),
18
+ h.AriaSetsize(place.setSize),
19
+ ...(place.isExpanded === undefined ? [] : [h.AriaExpanded(place.isExpanded)]),
20
+ ]), "@gjermundgaraba-designs/pixel-paper/src/rows/rows.js#rowRole");
21
+ /** A part the caller gives (a glyph, the actions) in its own cell, in a treegrid. */
22
+ const cellView = (config, className, content, h) => __foldkitBrandViewResult((config.treegrid === undefined
23
+ ? content
24
+ : slotView("span", className, content, h, cellRole(config, h))), "@gjermundgaraba-designs/pixel-paper/src/rows/rows.js#cellView");
10
25
  /** The row's text, with the words that say a context row is one. */
11
26
  const textOf = (config, h) => __foldkitBrandViewResult((config.isContext === true
12
27
  ? [...config.body, h.span([h.Class("sr-only")], [" (context)"])]
13
28
  : config.body), "@gjermundgaraba-designs/pixel-paper/src/rows/rows.js#textOf");
14
- const lineView = (config, box, text, h) => __foldkitBrandViewResult((h.div([...(config.attributes ?? []), h.Class(config.isContext === true ? "row context" : "row")], [
29
+ const lineView = (config, box, text, h) => __foldkitBrandViewResult((h.div([
30
+ ...(config.attributes ?? []),
31
+ ...rowRole(config.treegrid, h),
32
+ h.Class(config.isContext === true ? "row context" : "row"),
33
+ ], [
15
34
  ...box,
16
- ...(config.glyph === undefined ? [] : [config.glyph]),
35
+ ...cellView(config, "cell", config.glyph === undefined ? [] : [config.glyph], h),
17
36
  text,
18
- ...slotView("span", "slot", config.slot ?? [], h, config.slotAttributes),
19
- ...(config.actions ?? []),
20
- ...slotView("span", "tools", config.tools ?? [], h),
37
+ ...slotView("span", "slot", config.slot ?? [], h, [
38
+ ...(config.slotAttributes ?? []),
39
+ ...cellRole(config, h),
40
+ ]),
41
+ ...cellView(config, "cell actions", config.actions ?? [], h),
42
+ ...slotView("span", "tools", config.tools ?? [], h, cellRole(config, h)),
21
43
  ])), "@gjermundgaraba-designs/pixel-paper/src/rows/rows.js#lineView");
22
44
  const selectableLineView = (config, select, h) => __foldkitBrandViewResult((UiCheckbox.view({
23
45
  id: select.id,
24
46
  isChecked: select.isChecked,
25
47
  onToggle: select.onToggle,
26
48
  toView: (groups) => {
27
- const boxView = (name) => __foldkitBrandViewResult((h.span([h.Class("row-check")], [h.button(groups.checkbox), ...name])), "@gjermundgaraba-designs/pixel-paper/src/rows/rows.js#boxView");
49
+ const boxView = (name) => __foldkitBrandViewResult((h.span([...cellRole(config, h), h.Class("row-check")], [h.button(groups.checkbox), ...name])), "@gjermundgaraba-designs/pixel-paper/src/rows/rows.js#boxView");
50
+ const label = h.label([...groups.label, h.Class("grow")], textOf(config, h));
28
51
  return __foldkitBrandViewResult((select.label === undefined
29
- ? lineView(config, [boxView([])], h.label([...groups.label, h.Class("grow")], textOf(config, h)), h)
30
- : lineView(config, [boxView([h.span([...groups.label, h.Class("sr-only")], [select.label])])], h.div([h.Class("grow")], textOf(config, h)), h)), "@gjermundgaraba-designs/pixel-paper/src/rows/rows.js#toView");
52
+ ? lineView(config, [boxView([])], config.treegrid === undefined
53
+ ? label
54
+ : h.span([...cellRole(config, h), h.Class("cell grow-cell")], [label]), h)
55
+ : lineView(config, [boxView([h.span([...groups.label, h.Class("sr-only")], [select.label])])], h.div([...cellRole(config, h), h.Class("grow")], textOf(config, h)), h)), "@gjermundgaraba-designs/pixel-paper/src/rows/rows.js#toView");
31
56
  },
32
57
  }, h)), "@gjermundgaraba-designs/pixel-paper/src/rows/rows.js#selectableLineView");
33
58
  /** One 34px row: box, glyph, text, slot, actions, tools, and the rows nested under it. Hover
34
59
  * and focus tint it head. The box of a selectable row is built on `@foldkit/ui`'s Checkbox:
35
- * a `role="checkbox"` button (`aria-checked`) whose whole cell is its target. */
36
- export const rowView = (config, h) => __foldkitBrandViewResult((h.keyed("li")(config.key, [], [
37
- config.select === undefined
38
- ? lineView(config, [], h.div([h.Class("grow")], textOf(config, h)), h)
39
- : selectableLineView(config, config.select, h),
40
- ...slotView("ul", "rows", config.nested ?? [], h),
41
- ])), "@gjermundgaraba-designs/pixel-paper/src/rows/rows.js#rowView");
60
+ * a `role="checkbox"` button (`aria-checked`) whose whole cell is its target. In a treegrid
61
+ * the line is the `role="row"` and each part a `role="gridcell"`. */
62
+ export const rowView = (config, h) => {
63
+ const listRole = config.treegrid === undefined ? [] : [h.Role("none")];
64
+ return __foldkitBrandViewResult((h.keyed("li")(config.key, listRole, [
65
+ config.select === undefined
66
+ ? lineView(config, [], h.div([...cellRole(config, h), h.Class("grow")], textOf(config, h)), h)
67
+ : selectableLineView(config, config.select, h),
68
+ ...slotView("ul", "rows", config.nested ?? [], h, listRole),
69
+ ])), "@gjermundgaraba-designs/pixel-paper/src/rows/rows.js#rowView");
70
+ };
42
71
  /** A glyph in the row's 18px slot. */
43
72
  export const glyphView = (config, h) => __foldkitBrandViewResult((h.span([h.Class("glyph")], [Glyph.view({ name: config.name }, h)])), "@gjermundgaraba-designs/pixel-paper/src/rows/rows.js#glyphView");
44
73
  /** The glyph slot as a fold toggle, built on `@foldkit/ui`'s Button: a ring, open or closed,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjermundgaraba-designs/pixel-paper",
3
- "version": "0.12.0",
3
+ "version": "0.13.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -1401,6 +1401,20 @@ button.glyph {
1401
1401
  opacity: 1;
1402
1402
  }
1403
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
+
1404
1418
  .row .slot {
1405
1419
  min-width: 58px;
1406
1420
  flex: none;
@@ -1529,7 +1543,7 @@ button.glyph {
1529
1543
  inset: 0;
1530
1544
  }
1531
1545
 
1532
- .row > label.grow {
1546
+ .row label.grow {
1533
1547
  cursor: pointer;
1534
1548
  }
1535
1549
 
@@ -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
 
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";
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. */
@@ -72,12 +91,45 @@ export type RowConfig<Message> = Readonly<{
72
91
  isContext?: boolean;
73
92
  /** Makes the row selectable: a box at its left edge. */
74
93
  select?: RowSelect<Message>;
94
+ /** The row's place in a treegrid; every row of a treegrid has one. */
95
+ treegrid?: TreegridPlace;
75
96
  /** Further attributes for the row's line, the `div.row`, not its `li`: a keyboard list's
76
97
  * cursor (`aria-current="true"`, which the stylesheet outlines, and a roving `tabindex`),
77
- * `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. */
78
100
  attributes?: Attributes<Message>;
79
101
  }>;
80
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
+
81
133
  /** The row's text, with the words that say a context row is one. */
82
134
  const textOf = <Message>(
83
135
  config: RowConfig<Message>,
@@ -94,14 +146,21 @@ const lineView = <Message>(
94
146
  h: HtmlBuilder<Message>,
95
147
  ): Html =>
96
148
  h.div(
97
- [...(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
+ ],
98
154
  [
99
155
  ...box,
100
- ...(config.glyph === undefined ? [] : [config.glyph]),
156
+ ...cellView(config, "cell", config.glyph === undefined ? [] : [config.glyph], h),
101
157
  text,
102
- ...slotView("span", "slot", config.slot ?? [], h, config.slotAttributes),
103
- ...(config.actions ?? []),
104
- ...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)),
105
164
  ],
106
165
  );
107
166
 
@@ -117,19 +176,25 @@ const selectableLineView = <Message>(
117
176
  onToggle: select.onToggle,
118
177
  toView: (groups) => {
119
178
  const boxView = (name: ReadonlyArray<Html>): Html =>
120
- 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));
121
184
 
122
185
  return select.label === undefined
123
186
  ? lineView(
124
187
  config,
125
188
  [boxView([])],
126
- 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]),
127
192
  h,
128
193
  )
129
194
  : lineView(
130
195
  config,
131
196
  [boxView([h.span([...groups.label, h.Class("sr-only")], [select.label])])],
132
- h.div([h.Class("grow")], textOf(config, h)),
197
+ h.div([...cellRole(config, h), h.Class("grow")], textOf(config, h)),
133
198
  h,
134
199
  );
135
200
  },
@@ -139,18 +204,18 @@ const selectableLineView = <Message>(
139
204
 
140
205
  /** One 34px row: box, glyph, text, slot, actions, tools, and the rows nested under it. Hover
141
206
  * and focus tint it head. The box of a selectable row is built on `@foldkit/ui`'s Checkbox:
142
- * a `role="checkbox"` button (`aria-checked`) whose whole cell is its target. */
143
- export const rowView = <Message>(config: RowConfig<Message>, h: HtmlBuilder<Message>): Html =>
144
- h.keyed("li")(
145
- config.key,
146
- [],
147
- [
148
- config.select === undefined
149
- ? lineView(config, [], h.div([h.Class("grow")], textOf(config, h)), h)
150
- : selectableLineView(config, config.select, h),
151
- ...slotView("ul", "rows", config.nested ?? [], h),
152
- ],
153
- );
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
+ };
154
219
 
155
220
  // GLYPH SLOT
156
221