@gjermundgaraba-designs/pixel-paper 0.15.0 → 0.16.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
@@ -1037,7 +1037,7 @@ The printed button: a paper rectangle in a 2px ink frame that casts a 3px shadow
1037
1037
 
1038
1038
  ### Panels
1039
1039
 
1040
- - **Panel** → `section.panel[aria-labelledby] > header.panel-tb > h2 + span.meta`, then `div.panel-body` (or the body directly when `isFlush`), then `div.panel-foot`. The section is keyed by its `id`, so a panel among siblings that come and go keeps its own element, and a panel under another id is another element: one that replaces another in the same place is built anew (a live region inside it is inserted, not kept), so keep one panel under one id across states that should share it. Square, 2px ink border, Paper White, panel shadow. The title bar is inverted Departure Mono 13px uppercase 0.08em (`--tracking-title`), `padding: 6px 12px`; its meta is 11px at 75% opacity. A control in a title bar (a panel's, a window's or a card's) rings in paper, 2px inside its own edge, because the ink ring would vanish on the ink bar. The body pads 16px with a 14px grid gap; the foot pads `10px 12px` over a 1px rule, with a `.push` slot that goes right. `actions` puts small Buttons and IconButtons in `span.panel-acts` (6px apart) at the title bar's right end, after the meta; the bar trims to `3px 3px 3px 12px` at 31px or more. On the ink bar a button is a paper outline (a primary one fills paper), flat, nudged 1px when pressed, its ring 4px inside in its own colour; disabled, it prints at 60%.
1040
+ - **Panel** → `section.panel[aria-labelledby] > header.panel-tb > span.panel-lead + h2 + span.meta`, then `div.panel-body` (or the body directly when `isFlush`), then `div.panel-foot`. The section is keyed by its `id`, so a panel among siblings that come and go keeps its own element, and a panel under another id is another element: one that replaces another in the same place is built anew (a live region inside it is inserted, not kept), so keep one panel under one id across states that should share it. Square, 2px ink border, Paper White, panel shadow. The title bar is inverted Departure Mono 13px uppercase 0.08em (`--tracking-title`), `padding: 6px 12px`; its meta is 11px at 75% opacity. A control in a title bar (a panel's, a window's or a card's) rings in paper, 2px inside its own edge, because the ink ring would vanish on the ink bar. The body pads 16px with a 14px grid gap; the foot pads `10px 12px` over a 1px rule, with a `.push` slot that goes right. `actions` puts small Buttons and IconButtons in `span.panel-acts` (6px apart) at the title bar's right end, after the meta; the bar trims to `3px 3px 3px 12px` at 31px or more. `lead` puts a quiet IconButton in `span.panel-lead` before the title (a Sortable's grip), and the bar trims its left to 3px the same way; a quiet icon button there prints in paper, hovered too, and held at 60%. On the ink bar a button is a paper outline (a primary one fills paper), flat, nudged 1px when pressed, its ring 4px inside in its own colour; disabled, it prints at 60%.
1041
1041
  - **Deck** → `main.deck#main`: the column of panels.
1042
1042
  - **EmptyState** → `div.empty > h3.empty-title + p`: `headingLevel` is required and picks the title's element, which looks the same at every level. Where the empty state is a page's or a section's main content it is a heading, `3` inside a Panel (whose title is an `h2`), `1` for a gate or a missing page; `"Strong"` prints `strong.empty-title` where it is not (a chart with nothing to chart, a picture of the state). Centred, `26px 16px`, a 12px uppercase 0.08em (`--tracking-title`) headline, a muted 46ch sentence, an optional action 6px below.
1043
1043
  - **Wall** → `main.wall`, `Wall.markView` → `div.wall-mark` (a 30px ink-bordered glyph box beside the 22px wordmark). `Wall.cornerView` → `aside.wall-corner[aria-label]`, rendered just before the Wall: a paper slip in a 2px ink box on the 3px shadow, `6px 8px`, holding what a top bar would (see Layout).
@@ -1097,7 +1097,7 @@ One keyboard for the page, Vim first, with the arrows beside the letters. The ke
1097
1097
 
1098
1098
  ### Sortable
1099
1099
 
1100
- **Sortable** (Submodel over `@foldkit/ui`'s DragAndDrop, for items the reader puts in order: a list of rows or a grid of tiles) → `div.sortable (.grid)` holding `ul.sortable-items[aria-label]` (a plain list: no listbox, so an item's content may hold anything) of `li.sortable-item (.movable, .drag-dim, .drop-before, .drop-after)`, each a grip, then `div.sortable-body` with the parent's content, and the list's keyboard help in a `p.sr-only`. `"List"` (the default) prints rows edge to edge between 1px rules, for a flush panel, `8px 16px 8px 10px`, tinted head on hover; `"Grid"` prints tiles that wrap, 180px and up, 12px apart: paper slips in a 2px ink border on the 3px print shadow. Each item starts with a quiet `button.iconbtn` grip (the grip glyph, named "Move <label>", `aria-roledescription="draggable"`, `aria-pressed`, described by the keyboard help); while moves are held it stays, held (`aria-disabled`, in the keyboard), so focus on it stays put when holding moves cancels one. A mouse drags an item by its body or its grip (`cursor: grab`); touch takes the grip alone (`touch-action: none` on a movable grip only, so a finger on a row scrolls the page); the keyboard takes the grip too: Space or Enter picks the item up, every arrow moves it one place in reading order, Space or Enter drops it, Escape puts it back (taken before a dialog or the page's keys see it), and Tab leaves it where it is. A press on a control inside an item, or on anything `[data-no-lift]`, never lifts it. Only the pointer that pressed moves the item; the browser cancelling it puts the item back. The held item dims to 40% in place (`.drag-dim`) and keeps its own print; a copy of it, `.lifted`, tinted head on the 8px card shadow in a 2px ink border (`aria-hidden`, `inert`, rendered with `isLifted`, so it carries no ids), follows the pointer in `div.sortable-lift`, or, held by the keyboard, prints in the list where it would land, in `li.sortable-slot`, counted as no item. Where a pointer would drop it, a 2px dashed ink line prints beside the item it lands next to (`.drop-before`, or `.drop-after` past the last), over the layout, so nothing moves under the pointer. The pointer's place is read from the item whose centre is closest, across where items sit side by side and down a column, and read again where the pointer is let go, so a page that scrolled under a still pointer drops where the pointer is. Each step is said through the page's live region in words from the item's `label` and its place, counted from one ("Beta, 3 of 4."). The items are the parent's: a drop reports `Moved`, and the parent moves the item, or decides not to, and flashes it; a drop where the item was, outside the list, or while moves are held reports nothing. While a pointer presses or carries an item the page's root carries `data-pixel-paper-dragging`, which prints the grabbing cursor everywhere and holds text selection.
1100
+ **Sortable** (Submodel over `@foldkit/ui`'s DragAndDrop, for items the reader puts in order: a list of rows, a grid of tiles, or a grid of the page's panels) → `div.sortable (.grid, .panels)` holding `ul.sortable-items[aria-label]` (a plain list: no listbox, so an item's content may hold anything) of `li.sortable-item (.movable, .drag-dim, .drop-before, .drop-after)`, each a grip, then `div.sortable-body` with the parent's content, and the list's keyboard help in a `p.sr-only`. `"List"` (the default) prints rows edge to edge between 1px rules, for a flush panel, `8px 16px 8px 10px`, tinted head on hover; `"Grid"` prints tiles that wrap, 180px and up, 12px apart: paper slips in a 2px ink border on the 3px print shadow. `"Panels"` prints each item as the parent's own Panel, up to three across in the deck's 24px gutter and 300px and up, so two across and then one as the column narrows; a row takes its tallest panel's height, each panel's foot sits at its bottom, and the list hands each item its grip (`render.grip`) for the panel's `lead` instead of printing it beside the item. The held copy is the whole panel, tinted head on the 8px card shadow, under ids of its own, and the drop line runs down the middle of the gutter. `isReadOnly`, for a reader who may look but not reorder, prints every grip held and lifts nothing, as holding moves does. Each item starts with a quiet `button.iconbtn` grip (in a panel's title bar, for `"Panels"`) (the grip glyph, named "Move <label>", `aria-roledescription="draggable"`, `aria-pressed`, described by the keyboard help); while moves are held it stays, held (`aria-disabled`, in the keyboard), so focus on it stays put when holding moves cancels one. A mouse drags an item by its body or its grip (`cursor: grab`); touch takes the grip alone (`touch-action: none` on a movable grip only, so a finger on a row scrolls the page); the keyboard takes the grip too: Space or Enter picks the item up, every arrow moves it one place in reading order, Space or Enter drops it, Escape puts it back (taken before a dialog or the page's keys see it), and Tab leaves it where it is. A press on a control inside an item, or on anything `[data-no-lift]`, never lifts it. Only the pointer that pressed moves the item; the browser cancelling it puts the item back. The held item dims to 40% in place (`.drag-dim`) and keeps its own print; a copy of it, `.lifted`, tinted head on the 8px card shadow in a 2px ink border (`aria-hidden`, `inert`, rendered with `isLifted`, so it carries no ids), follows the pointer in `div.sortable-lift`, or, held by the keyboard, prints in the list where it would land, in `li.sortable-slot`, counted as no item. Where a pointer would drop it, a 2px dashed ink line prints beside the item it lands next to (`.drop-before`, or `.drop-after` past the last), over the layout, so nothing moves under the pointer. The pointer's place is read from the item whose centre is closest, across where items sit side by side and down a column, and read again where the pointer is let go, so a page that scrolled under a still pointer drops where the pointer is. Each step is said through the page's live region in words from the item's `label` and its place, counted from one ("Beta, 3 of 4."). The items are the parent's: a drop reports `Moved`, and the parent moves the item, or decides not to, and flashes it; a drop where the item was, outside the list, or while moves are held reports nothing. While a pointer presses or carries an item the page's root carries `data-pixel-paper-dragging`, which prints the grabbing cursor everywhere and holds text selection.
1101
1101
 
1102
1102
  ### Inputs / Fields
1103
1103
 
@@ -9,6 +9,9 @@ export type ViewConfig = Readonly<{
9
9
  id: string;
10
10
  /** Pixel caps in the inverted title bar. */
11
11
  title: string;
12
+ /** A quiet IconButton before the title, such as the grip a Sortable of panels hands each
13
+ * panel. On the ink bar it prints as `actions` do. */
14
+ lead?: ReadonlyArray<Html | string>;
12
15
  /** A count or state at the title bar's right. */
13
16
  meta?: string;
14
17
  /** Small Buttons and IconButtons at the title bar's right end, after the meta. On the ink
@@ -1 +1 @@
1
- {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../../src/panel/panel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAMtD,uDAAuD;AACvD,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC;;;;kCAI8B;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;yFACqF;IACrF,OAAO,CAAC,EAAE,aAAa,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;IACvC,IAAI,EAAE,aAAa,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;IACnC,2EAA2E;IAC3E,IAAI,CAAC,EAAE,aAAa,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;IACpC,uFAAuF;IACvF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC,CAAC;AAEH,iDAAiD;AACjD,eAAO,MAAM,OAAO,OAAQ,MAAM,KAAG,MAAuB,CAAC;AAE7D;gFACgF;AAChF,eAAO,MAAM,IAAI,GAAI,OAAO,UAAU,UAAU,KAAK,WAAW,CAAC,OAAO,CAAC,KAAG,IAmB3E,CAAC"}
1
+ {"version":3,"file":"panel.d.ts","sourceRoot":"","sources":["../../../src/panel/panel.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAMtD,uDAAuD;AACvD,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC;;;;kCAI8B;IAC9B,EAAE,EAAE,MAAM,CAAC;IACX,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd;2DACuD;IACvD,IAAI,CAAC,EAAE,aAAa,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;IACpC,iDAAiD;IACjD,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;yFACqF;IACrF,OAAO,CAAC,EAAE,aAAa,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;IACvC,IAAI,EAAE,aAAa,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;IACnC,2EAA2E;IAC3E,IAAI,CAAC,EAAE,aAAa,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;IACpC,uFAAuF;IACvF,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC,CAAC;AAEH,iDAAiD;AACjD,eAAO,MAAM,OAAO,OAAQ,MAAM,KAAG,MAAuB,CAAC;AAE7D;gFACgF;AAChF,eAAO,MAAM,IAAI,GAAI,OAAO,UAAU,UAAU,KAAK,WAAW,CAAC,OAAO,CAAC,KAAG,IAoB3E,CAAC"}
@@ -5,9 +5,10 @@ export const titleId = (id) => __foldkitBrandViewResult((`${id}-title`), "@gjerm
5
5
  /** The unit of the system: paper in a 2px ink box on a 5px print shadow, under a title bar. A
6
6
  * region landmark (`<section>`) named by its `h2` title, keyed by its `id`. */
7
7
  export const view = (config, h) => {
8
- const { id, title, meta, actions = [], body, foot = [], isFlush = false } = config;
8
+ const { id, title, lead = [], meta, actions = [], body, foot = [], isFlush = false } = config;
9
9
  return __foldkitBrandViewResult((h.keyed("section")(id, [h.Id(id), h.AriaLabelledBy(titleId(id)), h.Class("panel")], [
10
10
  h.header([h.Class("panel-tb")], [
11
+ ...slotView("span", "panel-lead", lead, h),
11
12
  h.h2([h.Id(titleId(id))], [title]),
12
13
  ...(meta === undefined ? [] : [h.span([h.Class("meta")], [meta])]),
13
14
  ...slotView("span", "panel-acts", actions, h),
@@ -954,13 +954,16 @@ export declare const SizeLiftedItem: Mount.MountDefinitionWithArgs<"SizeLiftedIt
954
954
  }, {
955
955
  readonly _tag: "CompletedSizeLiftedItem";
956
956
  }>;
957
- /** How the items lay out: a column of rows, or tiles that wrap. */
958
- export type Layout = "List" | "Grid";
957
+ /** How the items lay out: a column of rows, tiles that wrap, or the parent's own panels. */
958
+ export type Layout = "List" | "Grid" | "Panels";
959
959
  /** What the list hands `toItemView` for each item. */
960
960
  export type ItemRender = Readonly<{
961
961
  /** This render is the held copy: hidden from screen readers and inert. Leave out ids and
962
962
  * anything the page must find once. */
963
963
  isLifted: boolean;
964
+ /** The item's grip, for the `"Panels"` layout, where the item places it: in its panel's
965
+ * `lead`. The other layouts print it before the item themselves. */
966
+ grip: Html;
964
967
  }>;
965
968
  /** Per-render inputs passed to `view` through `h.submodel`'s `viewInputs`. The `to*` members
966
969
  * are functions because they may carry the parent's handlers. */
@@ -969,16 +972,21 @@ export type ViewInputs = Readonly<{
969
972
  /** Names the list for screen readers. */
970
973
  label: string;
971
974
  /** `"List"` (the default): rows between 1px rules, for a flush panel. `"Grid"`: tiles that
972
- * wrap, 180px and up. */
975
+ * wrap, 180px and up. `"Panels"`: each item is a Panel the parent prints, with `render.grip`
976
+ * in its `lead`, up to three across in the deck's gutter, 300px and up. */
973
977
  layout?: Layout;
978
+ /** The reader may look but not reorder, as a read-only sign-in: every grip prints held, as
979
+ * while moves are held, and nothing lifts. */
980
+ isReadOnly?: boolean;
974
981
  /** One item's content, beside its grip. Also called for the held copy, with
975
982
  * `render.isLifted`. */
976
983
  toItemView: (item: Item, render: ItemRender) => ReadonlyArray<Html>;
977
984
  }>;
978
985
  /**
979
- * Items the reader puts in order, as rows or as tiles, built on `@foldkit/ui`'s DragAndDrop:
980
- * `div.sortable` holding `ul.sortable-items` (named by `label`) of `li.sortable-item`, each its
981
- * grip and the parent's content. The items are the parent's; a drop reports `Moved`.
986
+ * Items the reader puts in order, as rows, as tiles or as panels, built on `@foldkit/ui`'s
987
+ * DragAndDrop: `div.sortable` holding `ul.sortable-items` (named by `label`) of
988
+ * `li.sortable-item`, each its grip and the parent's content (a panel holds its grip in its
989
+ * title bar). The items are the parent's; a drop reports `Moved`.
982
990
  *
983
991
  * A mouse drags an item by its body or its grip; touch takes the grip alone, so a finger on
984
992
  * an item scrolls the page; the keyboard takes the grip too: Space or Enter picks the item
@@ -1134,8 +1142,12 @@ export declare const view: import("foldkit/submodel").View<{
1134
1142
  /** Names the list for screen readers. */
1135
1143
  label: string;
1136
1144
  /** `"List"` (the default): rows between 1px rules, for a flush panel. `"Grid"`: tiles that
1137
- * wrap, 180px and up. */
1145
+ * wrap, 180px and up. `"Panels"`: each item is a Panel the parent prints, with `render.grip`
1146
+ * in its `lead`, up to three across in the deck's gutter, 300px and up. */
1138
1147
  layout?: Layout;
1148
+ /** The reader may look but not reorder, as a read-only sign-in: every grip prints held, as
1149
+ * while moves are held, and nothing lifts. */
1150
+ isReadOnly?: boolean;
1139
1151
  /** One item's content, beside its grip. Also called for the held copy, with
1140
1152
  * `render.isLifted`. */
1141
1153
  toItemView: (item: Item, render: ItemRender) => ReadonlyArray<Html>;
@@ -1 +1 @@
1
- {"version":3,"file":"sortable.d.ts","sourceRoot":"","sources":["../../../src/sortable/sortable.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"sortable.d.ts","sourceRoot":"","sources":["../../../src/sortable/sortable.ts"],"names":[],"mappings":"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"}
@@ -368,9 +368,15 @@ const helpId = (model) => __foldkitBrandViewResult((`${id(model)}-grip-help`), "
368
368
  const isHeldItem = (model, itemId) => __foldkitBrandViewResult((UiDragAndDrop.isDragging(model.dragAndDrop) &&
369
369
  Option.contains(UiDragAndDrop.maybeDraggedItemId(model.dragAndDrop), itemHook(model, itemId))), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#isHeldItem");
370
370
  const liftedGrip = (h) => __foldkitBrandViewResult((h.span([h.Class("iconbtn quiet"), h.AriaHidden(true)], [Glyph.view({ name: "grip" }, h)])), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#liftedGrip");
371
- const liftedView = (info, item, h) => __foldkitBrandViewResult((h.div([h.Class("sortable-item lifted")], [liftedGrip(h), h.div([h.Class("sortable-body")], info.toItemView(item, { isLifted: true }))])), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#liftedView");
371
+ /** The item's content beside its grip, or, in a layout where the item places its grip,
372
+ * alone. */
373
+ const itemContents = (info, item, render, h) => {
374
+ const body = h.div([h.Class("sortable-body")], info.toItemView(item, render));
375
+ return __foldkitBrandViewResult((info.layout === "Panels" ? [body] : [render.grip, body]), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#itemContents");
376
+ };
377
+ const liftedView = (info, item, h) => __foldkitBrandViewResult((h.div([h.Class("sortable-item lifted")], itemContents(info, item, { isLifted: true, grip: liftedGrip(h) }, h))), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#liftedView");
372
378
  const gripView = (info, item, h) => {
373
- const { model, items } = info;
379
+ const { model, isMovable, items } = info;
374
380
  const isIdleNow = phaseOf(model.dragAndDrop) === "Idle";
375
381
  const onGripKey = h.OnKeyDownPreventDefault((key) => __foldkitBrandViewResult((Option.liftPredicate(Message.PressedGripKey({ itemId: item.id, items }), () => __foldkitBrandViewResult((isIdleNow && (key === " " || key === "Enter")), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#anonymous~36"))), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#anonymous~35"));
376
382
  // NOTE: @foldkit/ui gap: DragAndDrop's `draggable` bundle forces `role="option"`, which a
@@ -381,19 +387,19 @@ const gripView = (info, item, h) => {
381
387
  glyph: "grip",
382
388
  label: fill(COPY.gripLabel, item.label),
383
389
  isQuiet: true,
384
- isDisabled: !model.isMovable,
390
+ isDisabled: !isMovable,
385
391
  attributes: [
386
392
  h.DataAttribute("draggable-id", itemHook(model, item.id)),
387
393
  h.DataAttribute(GRIP_ATTRIBUTE, ""),
388
394
  h.AriaRoleDescription("draggable"),
389
395
  h.AriaDescribedBy(helpId(model)),
390
396
  Button.pressedAttribute(isHeldItem(model, item.id), h),
391
- ...(model.isMovable ? [onGripKey] : []),
397
+ ...(isMovable ? [onGripKey] : []),
392
398
  ],
393
399
  }, h)), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#gripView");
394
400
  };
395
401
  const itemAttributes = (info, item, h) => {
396
- const { model, items } = info;
402
+ const { model, isMovable, items } = info;
397
403
  const isIdleNow = phaseOf(model.dragAndDrop) === "Idle";
398
404
  const onPress = h.OnPointerDown((pointerType, button, screenX, screenY, _time, clientX, clientY, pointerId, target) => __foldkitBrandViewResult((Option.liftPredicate(Message.PressedItem({
399
405
  itemId: item.id,
@@ -403,10 +409,7 @@ const itemAttributes = (info, item, h) => {
403
409
  clientX,
404
410
  clientY,
405
411
  items,
406
- }), () => __foldkitBrandViewResult((isIdleNow &&
407
- model.isMovable &&
408
- button === LEFT_BUTTON &&
409
- isLiftingPress(target, pointerType)), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#anonymous~38"))), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#anonymous~37"));
412
+ }), () => __foldkitBrandViewResult((isIdleNow && isMovable && button === LEFT_BUTTON && isLiftingPress(target, pointerType)), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#anonymous~38"))), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#anonymous~37"));
410
413
  return __foldkitBrandViewResult(([h.DataAttribute(ITEM_ATTRIBUTE, ""), onPress]), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#itemAttributes");
411
414
  };
412
415
  /** Where a pointer would drop the held item, marked on the item it would land beside: before
@@ -425,15 +428,12 @@ const dropClass = (info, item) => __foldkitBrandViewResult((Option.match(Option.
425
428
  onNone: () => __foldkitBrandViewResult((""), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#onNone~5"),
426
429
  onSome: ({ side }) => (__foldkitBrandViewResult((side === "Before" ? " drop-before" : " drop-after"), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#onSome~5")),
427
430
  })), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#dropClass");
428
- const itemClass = (info, item) => __foldkitBrandViewResult((`sortable-item${info.model.isMovable ? " movable" : ""}${isHeldItem(info.model, item.id) ? " drag-dim" : ""}${dropClass(info, item)}`), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#itemClass");
431
+ const itemClass = (info, item) => __foldkitBrandViewResult((`sortable-item${info.isMovable ? " movable" : ""}${isHeldItem(info.model, item.id) ? " drag-dim" : ""}${dropClass(info, item)}`), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#itemClass");
429
432
  const itemView = (info, item, h) => __foldkitBrandViewResult((h.keyed("li")(item.id, [
430
433
  ...UiDragAndDrop.sortable(itemHook(info.model, item.id)),
431
434
  ...itemAttributes(info, item, h),
432
435
  h.Class(itemClass(info, item)),
433
- ], [
434
- gripView(info, item, h),
435
- h.div([h.Class("sortable-body")], info.toItemView(item, { isLifted: false })),
436
- ])), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#itemView");
436
+ ], itemContents(info, item, { isLifted: false, grip: gripView(info, item, h) }, h))), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#itemView");
437
437
  /** The items in order, with the held item's copy where the keyboard would drop it: before the
438
438
  * other item at its target, or last. The copy carries no `data-sortable-id`, so it is not
439
439
  * counted as an item. */
@@ -473,10 +473,12 @@ const liftedCopyView = (info, h) => {
473
473
  })),
474
474
  ], [liftedView(info, held.item, h)])), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#anonymous~57")))), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#liftedCopyView");
475
475
  };
476
+ const layoutClass = (layout) => __foldkitBrandViewResult((Match.value(layout).pipe(Match.withReturnType(), Match.when("List", () => __foldkitBrandViewResult(("sortable"), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#anonymous~58")), Match.when("Grid", () => __foldkitBrandViewResult(("sortable grid"), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#anonymous~59")), Match.when("Panels", () => __foldkitBrandViewResult(("sortable panels"), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#anonymous~60")), Match.exhaustive)), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#layoutClass");
476
477
  /**
477
- * Items the reader puts in order, as rows or as tiles, built on `@foldkit/ui`'s DragAndDrop:
478
- * `div.sortable` holding `ul.sortable-items` (named by `label`) of `li.sortable-item`, each its
479
- * grip and the parent's content. The items are the parent's; a drop reports `Moved`.
478
+ * Items the reader puts in order, as rows, as tiles or as panels, built on `@foldkit/ui`'s
479
+ * DragAndDrop: `div.sortable` holding `ul.sortable-items` (named by `label`) of
480
+ * `li.sortable-item`, each its grip and the parent's content (a panel holds its grip in its
481
+ * title bar). The items are the parent's; a drop reports `Moved`.
480
482
  *
481
483
  * A mouse drags an item by its body or its grip; touch takes the grip alone, so a finger on
482
484
  * an item scrolls the page; the keyboard takes the grip too: Space or Enter picks the item
@@ -487,9 +489,16 @@ const liftedCopyView = (info, h) => {
487
489
  * the keys.
488
490
  */
489
491
  export const view = defineView((model, viewInputs, h) => {
490
- const { items, label, layout = "List", toItemView } = viewInputs;
491
- const info = { model, items, maybeDropMark: dropMarkOf(model), toItemView };
492
- return __foldkitBrandViewResult((h.div([h.Class(layout === "Grid" ? "sortable grid" : "sortable")], [
492
+ const { items, label, layout = "List", isReadOnly = false, toItemView } = viewInputs;
493
+ const info = {
494
+ model,
495
+ layout,
496
+ isMovable: model.isMovable && !isReadOnly,
497
+ items,
498
+ maybeDropMark: dropMarkOf(model),
499
+ toItemView,
500
+ };
501
+ return __foldkitBrandViewResult((h.div([h.Class(layoutClass(layout))], [
493
502
  // NOTE: Safari takes list semantics from a list printed without markers, so the list
494
503
  // says it is one
495
504
  h.ul([
@@ -500,5 +509,5 @@ export const view = defineView((model, viewInputs, h) => {
500
509
  ], itemsView(info, h)),
501
510
  h.p([h.Id(helpId(model)), h.Class("sr-only")], [COPY.gripHelp]),
502
511
  ...liftedCopyView(info, h),
503
- ])), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#anonymous~58");
512
+ ])), "@gjermundgaraba-designs/pixel-paper/src/sortable/sortable.js#anonymous~61");
504
513
  });
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gjermundgaraba-designs/pixel-paper",
3
- "version": "0.15.0",
3
+ "version": "0.16.0",
4
4
  "license": "MIT",
5
5
  "repository": {
6
6
  "type": "git",
@@ -14,6 +14,9 @@ export type ViewConfig = Readonly<{
14
14
  id: string;
15
15
  /** Pixel caps in the inverted title bar. */
16
16
  title: string;
17
+ /** A quiet IconButton before the title, such as the grip a Sortable of panels hands each
18
+ * panel. On the ink bar it prints as `actions` do. */
19
+ lead?: ReadonlyArray<Html | string>;
17
20
  /** A count or state at the title bar's right. */
18
21
  meta?: string;
19
22
  /** Small Buttons and IconButtons at the title bar's right end, after the meta. On the ink
@@ -32,7 +35,7 @@ export const titleId = (id: string): string => `${id}-title`;
32
35
  /** The unit of the system: paper in a 2px ink box on a 5px print shadow, under a title bar. A
33
36
  * region landmark (`<section>`) named by its `h2` title, keyed by its `id`. */
34
37
  export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html => {
35
- const { id, title, meta, actions = [], body, foot = [], isFlush = false } = config;
38
+ const { id, title, lead = [], meta, actions = [], body, foot = [], isFlush = false } = config;
36
39
 
37
40
  return h.keyed("section")(
38
41
  id,
@@ -41,6 +44,7 @@ export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html
41
44
  h.header(
42
45
  [h.Class("panel-tb")],
43
46
  [
47
+ ...slotView("span", "panel-lead", lead, h),
44
48
  h.h2([h.Id(titleId(id))], [title]),
45
49
  ...(meta === undefined ? [] : [h.span([h.Class("meta")], [meta])]),
46
50
  ...slotView("span", "panel-acts", actions, h),
@@ -983,23 +983,32 @@ a.textbtn {
983
983
  flex: 1 1 100%;
984
984
  }
985
985
 
986
- /* ---------- the panel's title-bar actions ---------- */
986
+ /* ---------- the panel's title-bar lead and actions ---------- */
987
987
 
988
- /* small buttons and icon buttons at the bar's right end; the bar trims its padding around
989
- them, as a window's does, so a small button keeps the bar near its one-line height */
990
- .panel-tb:has(> .panel-acts) {
988
+ /* small buttons and icon buttons at the bar's right end, and an icon button before the
989
+ title; the bar trims its padding around them, as a window's does, so a small button keeps
990
+ the bar near its one-line height */
991
+ .panel-tb:has(> :is(.panel-lead, .panel-acts)) {
991
992
  align-items: center;
992
993
  padding-block: 3px;
993
- padding-right: 3px;
994
994
  min-height: 31px;
995
995
  }
996
996
 
997
- .panel-tb:has(> .panel-acts) h2 {
997
+ .panel-tb:has(> .panel-lead) {
998
+ padding-left: 3px;
999
+ }
1000
+
1001
+ .panel-tb:has(> .panel-acts) {
1002
+ padding-right: 3px;
1003
+ }
1004
+
1005
+ .panel-tb:has(> :is(.panel-lead, .panel-acts)) h2 {
998
1006
  flex: 1;
999
1007
  min-width: 0;
1000
1008
  overflow-wrap: anywhere;
1001
1009
  }
1002
1010
 
1011
+ .panel-lead,
1003
1012
  .panel-acts {
1004
1013
  display: inline-flex;
1005
1014
  align-items: center;
@@ -1014,6 +1023,7 @@ a.textbtn {
1014
1023
 
1015
1024
  /* on the ink bar a button prints in paper: an outline, or filled when primary. No print
1016
1025
  shadow (it cannot show on ink), so a press nudges 1px, as a toolbar tool does */
1026
+ .panel-lead .btn,
1017
1027
  .panel-acts .btn {
1018
1028
  color: var(--paper);
1019
1029
  background: var(--ink);
@@ -1026,6 +1036,7 @@ a.textbtn {
1026
1036
  background: var(--paper);
1027
1037
  }
1028
1038
 
1039
+ .panel-lead .btn:not([aria-disabled="true"]):active,
1029
1040
  .panel-acts .btn:not([aria-disabled="true"]):active {
1030
1041
  transform: translate(1px, 1px);
1031
1042
  box-shadow: none;
@@ -1038,12 +1049,17 @@ a.textbtn {
1038
1049
  opacity: 0.6;
1039
1050
  }
1040
1051
 
1041
- .panel-acts .iconbtn.quiet {
1052
+ /* a quiet icon button prints in paper, and a hover cannot darken it to the ink of the bar */
1053
+ .panel-lead .iconbtn.quiet,
1054
+ .panel-acts .iconbtn.quiet,
1055
+ .panel-lead .iconbtn.quiet:hover,
1056
+ .panel-acts .iconbtn.quiet:hover {
1042
1057
  color: inherit;
1043
1058
  }
1044
1059
 
1045
1060
  /* the ring sits 4px inside the button in its own text colour, so it reads on an outline and
1046
1061
  on a filled one alike */
1062
+ .panel-lead .btn:focus-visible,
1047
1063
  .panel-acts .btn:focus-visible {
1048
1064
  outline-color: currentColor;
1049
1065
  outline-offset: -4px;
@@ -2692,8 +2708,8 @@ dialog.veil:focus {
2692
2708
  opacity: 0.75;
2693
2709
  }
2694
2710
 
2695
- /* a held close glyph fades on the bar, as a held button in a panel's title bar does */
2696
- .card-tb .iconbtn[aria-disabled="true"] {
2711
+ /* a held icon button fades on the bar, as a held button in a panel's title bar does */
2712
+ :is(.card-tb, .panel-tb) .iconbtn[aria-disabled="true"] {
2697
2713
  opacity: 0.6;
2698
2714
  cursor: default;
2699
2715
  }
@@ -3394,7 +3410,7 @@ dialog.veil:focus {
3394
3410
  bottom: -1px;
3395
3411
  }
3396
3412
 
3397
- .sortable.grid .sortable-item:is(.drop-before, .drop-after)::before {
3413
+ .sortable:is(.grid, .panels) .sortable-item:is(.drop-before, .drop-after)::before {
3398
3414
  top: -2px;
3399
3415
  bottom: -2px;
3400
3416
  left: auto;
@@ -3454,6 +3470,67 @@ dialog.veil:focus {
3454
3470
  padding: 0;
3455
3471
  }
3456
3472
 
3473
+ /* panels: the parent's own panels, up to three across in the deck's gutter and 300px and up,
3474
+ so two across and then one as the column narrows. A row takes its tallest panel's height,
3475
+ and each panel's foot sits at its bottom */
3476
+ .sortable.panels .sortable-items {
3477
+ display: grid;
3478
+ grid-template-columns: repeat(
3479
+ auto-fill,
3480
+ minmax(min(100%, max(300px, (100% - 2 * var(--gap)) / 3)), 1fr)
3481
+ );
3482
+ gap: var(--gap);
3483
+ }
3484
+
3485
+ .sortable.panels .sortable-item {
3486
+ display: block;
3487
+ padding: 0;
3488
+ border-bottom: 0;
3489
+ background: none;
3490
+ }
3491
+
3492
+ .sortable.panels .sortable-item.movable:hover {
3493
+ background: none;
3494
+ }
3495
+
3496
+ .sortable.panels .sortable-body {
3497
+ height: 100%;
3498
+ }
3499
+
3500
+ .sortable.panels .sortable-body > .panel {
3501
+ display: flex;
3502
+ flex-direction: column;
3503
+ height: 100%;
3504
+ }
3505
+
3506
+ .sortable.panels .sortable-body > .panel > .panel-foot {
3507
+ margin-top: auto;
3508
+ }
3509
+
3510
+ /* the drop line runs down the middle of the gutter */
3511
+ .sortable.panels .sortable-item.drop-before::before {
3512
+ left: calc(var(--gap) / -2 - 1px);
3513
+ }
3514
+
3515
+ .sortable.panels .sortable-item.drop-after::before {
3516
+ right: calc(var(--gap) / -2 - 1px);
3517
+ }
3518
+
3519
+ /* the held copy is the panel itself, tinted head and lifted onto the card shadow */
3520
+ .sortable.panels .sortable-item.lifted {
3521
+ border: 0;
3522
+ box-shadow: none;
3523
+ }
3524
+
3525
+ .sortable.panels .lifted .panel {
3526
+ background: var(--head);
3527
+ box-shadow: var(--shadow-card);
3528
+ }
3529
+
3530
+ .sortable.panels .sortable-slot {
3531
+ padding: 0;
3532
+ }
3533
+
3457
3534
  /* while a pointer carries an item, the page grabs and selects nothing */
3458
3535
  :root[data-pixel-paper-dragging] {
3459
3536
  user-select: none;
@@ -565,14 +565,17 @@ export const SizeLiftedItem = Mount.define("SizeLiftedItem", {
565
565
 
566
566
  // VIEW
567
567
 
568
- /** How the items lay out: a column of rows, or tiles that wrap. */
569
- export type Layout = "List" | "Grid";
568
+ /** How the items lay out: a column of rows, tiles that wrap, or the parent's own panels. */
569
+ export type Layout = "List" | "Grid" | "Panels";
570
570
 
571
571
  /** What the list hands `toItemView` for each item. */
572
572
  export type ItemRender = Readonly<{
573
573
  /** This render is the held copy: hidden from screen readers and inert. Leave out ids and
574
574
  * anything the page must find once. */
575
575
  isLifted: boolean;
576
+ /** The item's grip, for the `"Panels"` layout, where the item places it: in its panel's
577
+ * `lead`. The other layouts print it before the item themselves. */
578
+ grip: Html;
576
579
  }>;
577
580
 
578
581
  /** Per-render inputs passed to `view` through `h.submodel`'s `viewInputs`. The `to*` members
@@ -582,8 +585,12 @@ export type ViewInputs = Readonly<{
582
585
  /** Names the list for screen readers. */
583
586
  label: string;
584
587
  /** `"List"` (the default): rows between 1px rules, for a flush panel. `"Grid"`: tiles that
585
- * wrap, 180px and up. */
588
+ * wrap, 180px and up. `"Panels"`: each item is a Panel the parent prints, with `render.grip`
589
+ * in its `lead`, up to three across in the deck's gutter, 300px and up. */
586
590
  layout?: Layout;
591
+ /** The reader may look but not reorder, as a read-only sign-in: every grip prints held, as
592
+ * while moves are held, and nothing lifts. */
593
+ isReadOnly?: boolean;
587
594
  /** One item's content, beside its grip. Also called for the held copy, with
588
595
  * `render.isLifted`. */
589
596
  toItemView: (item: Item, render: ItemRender) => ReadonlyArray<Html>;
@@ -610,6 +617,8 @@ const isLiftingPress = (target: EventTarget | null, pointerType: string): boolea
610
617
 
611
618
  type ListInfo = Readonly<{
612
619
  model: Model;
620
+ layout: Layout;
621
+ isMovable: boolean;
613
622
  items: ReadonlyArray<Item>;
614
623
  maybeDropMark: Option.Option<DropMark>;
615
624
  toItemView: (item: Item, render: ItemRender) => ReadonlyArray<Html>;
@@ -624,6 +633,18 @@ const isHeldItem = (model: Model, itemId: string): boolean =>
624
633
  const liftedGrip = <ParentMessage>(h: HtmlBuilder<ParentMessage>): Html =>
625
634
  h.span([h.Class("iconbtn quiet"), h.AriaHidden(true)], [Glyph.view({ name: "grip" }, h)]);
626
635
 
636
+ /** The item's content beside its grip, or, in a layout where the item places its grip,
637
+ * alone. */
638
+ const itemContents = <ParentMessage>(
639
+ info: ListInfo,
640
+ item: Item,
641
+ render: ItemRender,
642
+ h: HtmlBuilder<ParentMessage>,
643
+ ): ReadonlyArray<Html> => {
644
+ const body = h.div([h.Class("sortable-body")], info.toItemView(item, render));
645
+ return info.layout === "Panels" ? [body] : [render.grip, body];
646
+ };
647
+
627
648
  const liftedView = <ParentMessage>(
628
649
  info: ListInfo,
629
650
  item: Item,
@@ -631,11 +652,11 @@ const liftedView = <ParentMessage>(
631
652
  ): Html =>
632
653
  h.div(
633
654
  [h.Class("sortable-item lifted")],
634
- [liftedGrip(h), h.div([h.Class("sortable-body")], info.toItemView(item, { isLifted: true }))],
655
+ itemContents(info, item, { isLifted: true, grip: liftedGrip(h) }, h),
635
656
  );
636
657
 
637
658
  const gripView = (info: ListInfo, item: Item, h: HtmlBuilder<Message>): Html => {
638
- const { model, items } = info;
659
+ const { model, isMovable, items } = info;
639
660
  const isIdleNow = phaseOf(model.dragAndDrop) === "Idle";
640
661
 
641
662
  const onGripKey: Attribute<Message> = h.OnKeyDownPreventDefault((key) =>
@@ -654,14 +675,14 @@ const gripView = (info: ListInfo, item: Item, h: HtmlBuilder<Message>): Html =>
654
675
  glyph: "grip",
655
676
  label: fill(COPY.gripLabel, item.label),
656
677
  isQuiet: true,
657
- isDisabled: !model.isMovable,
678
+ isDisabled: !isMovable,
658
679
  attributes: [
659
680
  h.DataAttribute("draggable-id", itemHook(model, item.id)),
660
681
  h.DataAttribute(GRIP_ATTRIBUTE, ""),
661
682
  h.AriaRoleDescription("draggable"),
662
683
  h.AriaDescribedBy(helpId(model)),
663
684
  Button.pressedAttribute(isHeldItem(model, item.id), h),
664
- ...(model.isMovable ? [onGripKey] : []),
685
+ ...(isMovable ? [onGripKey] : []),
665
686
  ],
666
687
  },
667
688
  h,
@@ -673,7 +694,7 @@ const itemAttributes = (
673
694
  item: Item,
674
695
  h: HtmlBuilder<Message>,
675
696
  ): ReadonlyArray<Attribute<Message>> => {
676
- const { model, items } = info;
697
+ const { model, isMovable, items } = info;
677
698
  const isIdleNow = phaseOf(model.dragAndDrop) === "Idle";
678
699
 
679
700
  const onPress = h.OnPointerDown(
@@ -689,10 +710,7 @@ const itemAttributes = (
689
710
  items,
690
711
  }),
691
712
  () =>
692
- isIdleNow &&
693
- model.isMovable &&
694
- button === LEFT_BUTTON &&
695
- isLiftingPress(target, pointerType),
713
+ isIdleNow && isMovable && button === LEFT_BUTTON && isLiftingPress(target, pointerType),
696
714
  ),
697
715
  );
698
716
 
@@ -739,7 +757,7 @@ const dropClass = (info: ListInfo, item: Item): string =>
739
757
  );
740
758
 
741
759
  const itemClass = (info: ListInfo, item: Item): string =>
742
- `sortable-item${info.model.isMovable ? " movable" : ""}${isHeldItem(info.model, item.id) ? " drag-dim" : ""}${dropClass(info, item)}`;
760
+ `sortable-item${info.isMovable ? " movable" : ""}${isHeldItem(info.model, item.id) ? " drag-dim" : ""}${dropClass(info, item)}`;
743
761
 
744
762
  const itemView = (info: ListInfo, item: Item, h: HtmlBuilder<Message>): Html =>
745
763
  h.keyed("li")(
@@ -749,10 +767,7 @@ const itemView = (info: ListInfo, item: Item, h: HtmlBuilder<Message>): Html =>
749
767
  ...itemAttributes(info, item, h),
750
768
  h.Class(itemClass(info, item)),
751
769
  ],
752
- [
753
- gripView(info, item, h),
754
- h.div([h.Class("sortable-body")], info.toItemView(item, { isLifted: false })),
755
- ],
770
+ itemContents(info, item, { isLifted: false, grip: gripView(info, item, h) }, h),
756
771
  );
757
772
 
758
773
  /** The items in order, with the held item's copy where the keyboard would drop it: before the
@@ -819,10 +834,20 @@ const liftedCopyView = (info: ListInfo, h: HtmlBuilder<Message>): ReadonlyArray<
819
834
  );
820
835
  };
821
836
 
837
+ const layoutClass = (layout: Layout): string =>
838
+ Match.value(layout).pipe(
839
+ Match.withReturnType<string>(),
840
+ Match.when("List", () => "sortable"),
841
+ Match.when("Grid", () => "sortable grid"),
842
+ Match.when("Panels", () => "sortable panels"),
843
+ Match.exhaustive,
844
+ );
845
+
822
846
  /**
823
- * Items the reader puts in order, as rows or as tiles, built on `@foldkit/ui`'s DragAndDrop:
824
- * `div.sortable` holding `ul.sortable-items` (named by `label`) of `li.sortable-item`, each its
825
- * grip and the parent's content. The items are the parent's; a drop reports `Moved`.
847
+ * Items the reader puts in order, as rows, as tiles or as panels, built on `@foldkit/ui`'s
848
+ * DragAndDrop: `div.sortable` holding `ul.sortable-items` (named by `label`) of
849
+ * `li.sortable-item`, each its grip and the parent's content (a panel holds its grip in its
850
+ * title bar). The items are the parent's; a drop reports `Moved`.
826
851
  *
827
852
  * A mouse drags an item by its body or its grip; touch takes the grip alone, so a finger on
828
853
  * an item scrolls the page; the keyboard takes the grip too: Space or Enter picks the item
@@ -833,11 +858,18 @@ const liftedCopyView = (info: ListInfo, h: HtmlBuilder<Message>): ReadonlyArray<
833
858
  * the keys.
834
859
  */
835
860
  export const view = defineView<Model, Message, ViewInputs>((model, viewInputs, h): Html => {
836
- const { items, label, layout = "List", toItemView } = viewInputs;
837
- const info: ListInfo = { model, items, maybeDropMark: dropMarkOf(model), toItemView };
861
+ const { items, label, layout = "List", isReadOnly = false, toItemView } = viewInputs;
862
+ const info: ListInfo = {
863
+ model,
864
+ layout,
865
+ isMovable: model.isMovable && !isReadOnly,
866
+ items,
867
+ maybeDropMark: dropMarkOf(model),
868
+ toItemView,
869
+ };
838
870
 
839
871
  return h.div(
840
- [h.Class(layout === "Grid" ? "sortable grid" : "sortable")],
872
+ [h.Class(layoutClass(layout))],
841
873
  [
842
874
  // NOTE: Safari takes list semantics from a list printed without markers, so the list
843
875
  // says it is one