@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 +3 -3
- package/dist/src/places/places.d.ts +15 -5
- package/dist/src/places/places.d.ts.map +1 -1
- package/dist/src/places/places.js +19 -17
- package/dist/src/rows/index.d.ts +1 -1
- package/dist/src/rows/index.d.ts.map +1 -1
- package/dist/src/rows/rows.d.ts +24 -3
- package/dist/src/rows/rows.d.ts.map +1 -1
- package/dist/src/rows/rows.js +44 -15
- package/package.json +1 -1
- package/src/pixel-paper.css +15 -1
- package/src/places/places.ts +43 -30
- package/src/rows/index.ts +1 -0
- package/src/rows/rows.ts +87 -22
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.
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
|
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
|
|
7
|
-
const { label, isCurrent = false, count
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
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
|
|
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");
|
package/dist/src/rows/index.d.ts
CHANGED
|
@@ -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"}
|
package/dist/src/rows/rows.d.ts
CHANGED
|
@@ -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"`
|
|
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":"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,
|
|
1
|
+
{"version":3,"file":"rows.d.ts","sourceRoot":"","sources":["../../../src/rows/rows.ts"],"names":[],"mappings":"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"}
|
package/dist/src/rows/rows.js
CHANGED
|
@@ -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([
|
|
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,
|
|
19
|
-
|
|
20
|
-
|
|
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([])],
|
|
30
|
-
|
|
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
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
])
|
|
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
package/src/pixel-paper.css
CHANGED
|
@@ -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
|
|
1546
|
+
.row label.grow {
|
|
1533
1547
|
cursor: pointer;
|
|
1534
1548
|
}
|
|
1535
1549
|
|
package/src/places/places.ts
CHANGED
|
@@ -7,8 +7,11 @@ import { slotView } from "../slot/slot.ts";
|
|
|
7
7
|
|
|
8
8
|
// VIEW
|
|
9
9
|
|
|
10
|
-
/** One place in Places: a button that shows it
|
|
11
|
-
|
|
10
|
+
/** One place in Places: a link to its address (`href`), or a button that shows it in the page
|
|
11
|
+
* (`onSelect`). */
|
|
12
|
+
export type Place<Message> = PlaceContent & PlaceTarget<Message>;
|
|
13
|
+
|
|
14
|
+
type PlaceContent = Readonly<{
|
|
12
15
|
/** Stable and unique among the places: the row's key, so two places that share a name stay
|
|
13
16
|
* apart. */
|
|
14
17
|
id: string;
|
|
@@ -20,10 +23,20 @@ export type Place<Message> = Readonly<{
|
|
|
20
23
|
count?: number;
|
|
21
24
|
/** A quiet IconButton after it that acts on the place itself (remove a saved filter). */
|
|
22
25
|
tools?: ReadonlyArray<Html>;
|
|
23
|
-
/** Shows the place. */
|
|
24
|
-
onSelect: Message;
|
|
25
26
|
}>;
|
|
26
27
|
|
|
28
|
+
type PlaceTarget<Message> =
|
|
29
|
+
| Readonly<{
|
|
30
|
+
/** The place's address: the line is a link, so it opens in a new tab and reads as one. */
|
|
31
|
+
href: string;
|
|
32
|
+
onSelect?: never;
|
|
33
|
+
}>
|
|
34
|
+
| Readonly<{
|
|
35
|
+
/** Shows the place without leaving the page: the line is a button. */
|
|
36
|
+
onSelect: Message;
|
|
37
|
+
href?: never;
|
|
38
|
+
}>;
|
|
39
|
+
|
|
27
40
|
/** Configuration for rendering places with `view`. */
|
|
28
41
|
export type ViewConfig<Message> = Readonly<{
|
|
29
42
|
/** Names the navigation for screen readers ("Areas"). */
|
|
@@ -31,39 +44,39 @@ export type ViewConfig<Message> = Readonly<{
|
|
|
31
44
|
places: ReadonlyArray<Place<Message>>;
|
|
32
45
|
}>;
|
|
33
46
|
|
|
47
|
+
const lineView = <Message>(place: Place<Message>, h: HtmlBuilder<Message>): Html => {
|
|
48
|
+
const { label, isCurrent = false, count } = place;
|
|
49
|
+
const current = isCurrent ? [h.AriaCurrent("page")] : [];
|
|
50
|
+
const content = [
|
|
51
|
+
h.span([h.Class("place-name")], [label]),
|
|
52
|
+
...(count === undefined
|
|
53
|
+
? []
|
|
54
|
+
: [" ", h.span([h.Class("place-count")], [count.toLocaleString(LOCALE)])]),
|
|
55
|
+
];
|
|
56
|
+
|
|
57
|
+
return place.href === undefined
|
|
58
|
+
? UiButton.view(
|
|
59
|
+
{
|
|
60
|
+
onClick: place.onSelect,
|
|
61
|
+
toView: (groups) => h.button([...groups.button, ...current], content),
|
|
62
|
+
},
|
|
63
|
+
h,
|
|
64
|
+
)
|
|
65
|
+
: h.a([h.Href(place.href), ...current], content);
|
|
66
|
+
};
|
|
67
|
+
|
|
34
68
|
const placeView =
|
|
35
69
|
<Message>(h: HtmlBuilder<Message>) =>
|
|
36
|
-
(place: Place<Message>): Html =>
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
return h.keyed("li")(
|
|
70
|
+
(place: Place<Message>): Html =>
|
|
71
|
+
h.keyed("li")(
|
|
40
72
|
place.id,
|
|
41
73
|
[],
|
|
42
|
-
[
|
|
43
|
-
UiButton.view(
|
|
44
|
-
{
|
|
45
|
-
onClick: onSelect,
|
|
46
|
-
toView: (groups) =>
|
|
47
|
-
h.button(
|
|
48
|
-
[...groups.button, ...(isCurrent ? [h.AriaCurrent("page")] : [])],
|
|
49
|
-
[
|
|
50
|
-
h.span([h.Class("place-name")], [label]),
|
|
51
|
-
...(count === undefined
|
|
52
|
-
? []
|
|
53
|
-
: [" ", h.span([h.Class("place-count")], [count.toLocaleString(LOCALE)])]),
|
|
54
|
-
],
|
|
55
|
-
),
|
|
56
|
-
},
|
|
57
|
-
h,
|
|
58
|
-
),
|
|
59
|
-
...slotView("span", "place-tools", tools, h),
|
|
60
|
-
],
|
|
74
|
+
[lineView(place, h), ...slotView("span", "place-tools", place.tools ?? [], h)],
|
|
61
75
|
);
|
|
62
|
-
};
|
|
63
76
|
|
|
64
77
|
/** A column of places to go (areas, collections, saved filters), flush in a rail panel: a
|
|
65
|
-
* navigation landmark named by its label, one line each a button built
|
|
66
|
-
* Button, the one you are in `aria-current="page"` and printed inverted. */
|
|
78
|
+
* navigation landmark named by its label, one line each a link to the place or a button built
|
|
79
|
+
* on `@foldkit/ui`'s Button, the one you are in `aria-current="page"` and printed inverted. */
|
|
67
80
|
export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Message>): Html => {
|
|
68
81
|
const { label, places } = config;
|
|
69
82
|
|
package/src/rows/index.ts
CHANGED
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"`
|
|
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
|
-
[
|
|
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,
|
|
103
|
-
|
|
104
|
-
|
|
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(
|
|
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
|
-
|
|
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
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
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
|
|