@gjermundgaraba-designs/pixel-paper 0.20.1 → 0.21.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/field/field.d.ts +3 -0
- package/dist/src/field/field.d.ts.map +1 -1
- package/dist/src/field/field.js +2 -2
- package/dist/src/lineDiff/index.d.ts +2 -0
- package/dist/src/lineDiff/index.d.ts.map +1 -1
- package/dist/src/lineDiff/index.js +1 -0
- package/dist/src/lineDiff/lineDiff.d.ts +8 -4
- package/dist/src/lineDiff/lineDiff.d.ts.map +1 -1
- package/dist/src/lineDiff/lineDiff.js +19 -15
- package/dist/src/lineDiff/patch.d.ts +35 -0
- package/dist/src/lineDiff/patch.d.ts.map +1 -0
- package/dist/src/lineDiff/patch.js +146 -0
- package/dist/src/menu/menu.d.ts +6 -6
- package/dist/src/sortable/sortable.d.ts +30 -30
- package/dist/src/toast/toast.d.ts +12 -12
- package/dist/src/window/window.d.ts +3 -1
- package/dist/src/window/window.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/field/field.ts +5 -1
- package/src/lineDiff/index.ts +2 -0
- package/src/lineDiff/lineDiff.ts +29 -24
- package/src/lineDiff/patch.ts +218 -0
- package/src/pixel-paper.css +6 -0
- package/src/window/window.ts +3 -1
package/DESIGN.md
CHANGED
|
@@ -1057,7 +1057,7 @@ The printed button: a paper rectangle in a 2px ink frame that casts a 3px shadow
|
|
|
1057
1057
|
- **Banner** → `div.banner > span.glyph (.error, .ok, .quiet) + span`: a fixed paper slip on the 3px shadow with a 26px glyph box in the Notice's tones and a sentence that says what happens to the reader's work. It holds a state the whole interface is in, for as long as it lasts, one at a time. It is not a live region (it arrives already holding its sentence, which would go unheard): the application says the state once, when it begins, with the `Announcer.Announce` Command. `"Error"` (the default; red, bang): a state that stops work (offline). `"Warn"` (amber): a state that is passing (reconnecting). `"Ok"` (ink, check): the state just ended (back online), shown briefly and then taken away. `"Quiet"` (head, info): news. With the connection word it is one pair: LinkState `"Reconnecting"` beside a warn Banner, `"Offline"` beside an error Banner with a retry, then `"Online"` beside an ok Banner for a moment; `"Connecting"` takes none.
|
|
1058
1058
|
- **Rail** (Submodel) → `div.railed > main.deck#main + aside.rail[popover="auto"][aria-label]`: the rail's panels (a tree, places, filters) beside the deck's, after it in the markup so the page's main content and its h1 come first in reading order; the stylesheet fixes the rail at the left under the top bar. One element serves both layouts, so nothing in it renders twice or loses its state: at rest it stands there (its `popover` attribute is inert until opened), and at 900px and under it is hidden and `span.rail-open > button.btn.small[aria-expanded]` at the top of the deck opens it as a drawer: a slice of the dithered ground, `min(320px, 100vw - 40px)` wide from the left edge, closed by a 2px ink rule and the 8px card shadow, entering from the left in three steps over a veil. Its head (`div.rail-head`: the name in the label register and a close IconButton) shows only in the drawer. Opening focuses its first control; Escape, a click on the veil, the close glyph or focus leaving closes it, and all but the last return focus to the button. Widening the window past 900px closes it. The parent reads whether the drawer is open with `Rail.isOpen`, to hold its own page keys while the drawer covers the page. A file tree flush in a rail panel (or any panel) drops its own border.
|
|
1059
1059
|
- **Stage** → `main.stage#main > div.stage-work > div.stage-canvas + div.stage-tools + div.stage-bar`: see Layout. `paused` (a Notice saying why: the link dropped, the page is read-only) veils the work when editing stops: `div.stage-paused[role="status"]` lays the dialog veil and its ink grain over everything and centres that Notice on the 3px button shadow, while the work beneath is `inert`. It lifts when `paused` is not given; the connection word says reconnecting meanwhile.
|
|
1060
|
-
- **Window** → `section.panel.window (.flush) > header.panel-tb + div.window-body + div.panel-foot`: a Panel for a stage or a reel, as tall and wide as whatever places it (and moved by it, if anything moves it). Its title bar is the panel's, trimmed to hold tools: 30px tall (`3px 3px 3px 10px`, its left trimmed to 3px under a `lead`), centred, with an optional `lead` (`span.panel-lead`, a quiet IconButton: a Reel's grip), an optional kind glyph, the title (clipped with an ellipsis), a status chip (its border turns paper so it reads on ink), tools and a close IconButton, both in the bar's paper. The body scrolls inside (`16px`, a 14px grid); flush, what fills it (a code surface, a file tree, a diff) drops its border and fills it. A foot is a panel foot; a notice there takes the whole line.
|
|
1060
|
+
- **Window** → `section.panel.window (.flush) > header.panel-tb + div.window-body + div.panel-foot`: a Panel for a stage or a reel, as tall and wide as whatever places it (and moved by it, if anything moves it). Its title bar is the panel's, trimmed to hold tools: 30px tall (`3px 3px 3px 10px`, its left trimmed to 3px under a `lead`), centred, with an optional `lead` (`span.panel-lead`, a quiet IconButton: a Reel's grip), an optional kind glyph, the title (clipped with an ellipsis), a status chip (its border turns paper so it reads on ink), tools and a close IconButton, both in the bar's paper. Tools a child Submodel hands back come as one element, `span.window-tools`, which is no box of its own (`display: contents`), so its buttons sit in the bar as the others do. The body scrolls inside (`16px`, a 14px grid); flush, what fills it (a code surface, a file tree, a diff) drops its border and fills it. A foot is a panel foot; a notice there takes the whole line.
|
|
1061
1061
|
- **Loading** → `div.loading[role="status"]`: where content is on its way, centred like an EmptyState at `26px 16px`, a 12px uppercase 0.08em (`--tracking-title`) muted line ("loading the items") ending in the 6×11px working cursor. Its status element stays in the page and only its words change: with `isLoading` false it is `div.loading-idle[role="status"]`, empty and out of the flow (`position: absolute`, never `display: none`, which would drop the live region), so it takes no room and no gap at the top of a panel's or a card's body, and a screen reader hears the line when it arrives. The content or an EmptyState takes its place on screen; the line never prints beside either. It is not the page's live region (the Announcer's).
|
|
1062
1062
|
- **Remote** → spread where content goes: data that loads, in each of Foldkit's `AsyncData` states, with the retry built in, so a failure is never shown without its way back. A Loading line (`loading <label>`) leads in every state, in place, so its status is heard, and holds its words only while the first load runs: a refresh over data already shown is quiet, so a page that polls is not heard each time. Retry takes its button away with the notice, so the update returns focus to the line (its `id`) with the retry, where a first load is heard. A failure with nothing to show is an error Notice titled "<Label> not loaded", the reason (`toErrorText`) under it and a small "retry" button (`onRetry`); a refresh that failed over data already shown (stale) is a warn Notice titled "<Label> not refreshed" with the same reason and retry, above the last data; loaded and refreshing data is the caller's `toDataView` alone, an EmptyState for none included.
|
|
1063
1063
|
- **Disclosure** (a View over `@foldkit/ui`'s Disclosure; the caller keeps whether it is open) → `div.disclosure > button.disclosure-summary[aria-expanded] + div.disclosure-body`. The summary is a control-register line (`3px 0`) of a 12px `caret-right` glyph, `span.disclosure-label` (a dotted underline on hover) and an optional `span.meta` pushed right; open, the caret turns a quarter in one step (a right angle keeps the grid). The body hangs from the caret on a 1px Rule Grey guide, its content level with the summary's words (`margin-left: 5px`, `padding-left: 14px`), in a 14px grid. For what a reader opens on purpose (grants to edit, details, a danger zone), never to hide the one thing the screen is for.
|
|
@@ -1130,7 +1130,7 @@ Focus is the reel's: one pane is focused (`.focused`, its window on the 8px card
|
|
|
1130
1130
|
|
|
1131
1131
|
### Inputs / Fields
|
|
1132
1132
|
|
|
1133
|
-
- **Field** (a View over `@foldkit/ui`'s Input) → `div.field (.boxed, .mono) > label + input + p.field-error + p.field-help`, the label naming the input and the help describing it; a failed field prints why in `p.field-error` (sans, Urgent Red) under the control, which is `aria-invalid` and described by it before its help, and the ErrorSummary above the fields says it again; a required field's control is `aria-required`; the input's `type` the platform's (`"date"` for the browser's date control) when given. **SelectField** (over `@foldkit/ui`'s Select) → `div.field > label + select + p.field-error + p.field-help`, its options in order and a group's under an `optgroup` heading, an option that is listed but not choosable `disabled`; **TextareaField** (over `@foldkit/ui`'s Textarea) → `div.field (.mono) > label + textarea + p.field-error + p.field-help`, opening at `rows` lines. Both name, describe and mark their control as a Field does. **Fieldset** → `fieldset.field > legend.field-label`, then the controls, `p.field-error` and `p.field-help`, its error and its help linked with `aria-describedby`; **FieldGrid** → `div.field-grid` (auto-fit, 160px minimum); **Fields** → `fieldset.stack.gap-m`: a form's fields as one column `--space-m` (14px) apart, a body's gap, on the layout primitives with no rule of its own. `isBusy` (a save in flight) sets `disabled` and `aria-busy` on it, so every control holds at once and nothing changes under the save; the submit Button stays outside it, `isBusy`, so it keeps focus. A disabled control prints its value in Muted Ink; a boxless one also drops its dotted baseline (the mark of a place to type), a boxed one or a select mutes its box and caret.
|
|
1133
|
+
- **Field** (a View over `@foldkit/ui`'s Input) → `div.field (.boxed, .mono) > label + input + p.field-error + p.field-help`, the label naming the input and the help describing it; a failed field prints why in `p.field-error` (sans, Urgent Red) under the control, which is `aria-invalid` and described by it before its help, and the ErrorSummary above the fields says it again; a required field's control is `aria-required`; the input's `type` the platform's (`"date"` for the browser's date control) when given. A field whose place says what it is (a browser's address bar) hides its label (`label.sr-only`), which still names the input. **SelectField** (over `@foldkit/ui`'s Select) → `div.field > label + select + p.field-error + p.field-help`, its options in order and a group's under an `optgroup` heading, an option that is listed but not choosable `disabled`; **TextareaField** (over `@foldkit/ui`'s Textarea) → `div.field (.mono) > label + textarea + p.field-error + p.field-help`, opening at `rows` lines. Both name, describe and mark their control as a Field does. **Fieldset** → `fieldset.field > legend.field-label`, then the controls, `p.field-error` and `p.field-help`, its error and its help linked with `aria-describedby`; **FieldGrid** → `div.field-grid` (auto-fit, 160px minimum); **Fields** → `fieldset.stack.gap-m`: a form's fields as one column `--space-m` (14px) apart, a body's gap, on the layout primitives with no rule of its own. `isBusy` (a save in flight) sets `disabled` and `aria-busy` on it, so every control holds at once and nothing changes under the save; the submit Button stays outside it, `isBusy`, so it keeps focus. A disabled control prints its value in Muted Ink; a boxless one also drops its dotted baseline (the mark of a place to type), a boxed one or a select mutes its box and caret.
|
|
1134
1134
|
- **Label:** 10px uppercase 0.1em Muted Ink, 4px above; turns red when the field is invalid, as a fieldset's legend does when a control in it is.
|
|
1135
1135
|
- **Boxless (default):** full width, sans body, no box, a 1px dotted Muted Ink baseline, `padding: 2px 0`. On keyboard the baseline becomes 2px solid ink; invalid, 2px solid red.
|
|
1136
1136
|
- **Boxed:** the field that is the whole job of its form (sign-in, a quick-add field): 2px ink border, `padding: 5px 8px`; keyboard ring 2px outside; invalid turns the border red. A date is always boxed.
|
|
@@ -1164,7 +1164,7 @@ Focus is the reel's: one pane is focused (`.focused`, its window on the 8px card
|
|
|
1164
1164
|
- **CodeSurface** → `pre.code-surface[tabindex="0"][role="group"]`, named by its label: a 2px ink-bordered head-tinted block in 12px/1.6 system mono, `10px 12px`, `tab-size: 2`, wrapping. It takes a string or a highlighter's spans: `tok-keyword`; `tok-string`, `tok-string2`, `tok-url`; `tok-number`, `tok-bool`, `tok-atom`, `tok-literal`; `tok-definition`, `tok-variableName2`, `tok-macroName`, `tok-link` (underlined); `tok-typeName`, `tok-className`, `tok-namespace`; `tok-comment`, `tok-meta`, `tok-punctuation` take their syntax roles; `tok-heading` and `tok-strong` are bold, `tok-emphasis` italic, `tok-inserted` green ink, `tok-deleted` and `tok-invalid` red ink (invalid on a dotted underline). Anything unnamed stays ink. **CodeEditor** (a View over `@foldkit/ui`'s Textarea) → `textarea.code-surface`, spelling, capitals and autocomplete off: 220px minimum, grows to 60vh, vertically resizable, scrolls sideways (wraps on phones).
|
|
1165
1165
|
- **Secret** → `div.secret > code[role="group"] + button.btn.small`, the code named by its label: the same box at `6px 6px 6px 12px` with a scrolling nowrap code at 0.02em (wrapping anywhere on a phone, so the end is never hidden) and, in its `actions`, a copy button whose word turns "copied". **CopyButton** (Submodel; `CopyButton.isCopied` is for a parent that follows the copy) → the quiet copy glyph (a check once copied) or, as `"Button"`, a small button whose word turns "copied", for a beat; either says it to screen readers, and a copy that fails says so. It writes through Effect's `Clipboard` service: the application provides `Clipboard.layer` from `@effect/platform-browser` in its `resources`. A `div.save-row` with a `span.push` holds what follows it (the "I've stored it" commit).
|
|
1166
1166
|
- **History** → `ol.history-list (.checks) > li (.current, [aria-current="true"]) > span.v + span.who + span.what (.failed)`: ruled rows, the version in 11px pixel face in a fixed `4ch + 8px` box (inverted when current, so nothing moves, a Time in it taking the paper too), who in 10px muted at 10ch (20ch for checks), what in sans, a failed line red with "(failed)" after it for screen readers (`span.sr-only`), so the state never rests on colour. `isWrapped` → `ol.history-list.wrapped` (never `.wrap`, which is Wrap's row), for an activity log whose words matter more than a tidy column, compact enough for a rail's panel: when (a whole timestamp, its box as wide as it, muted and flush with the what under it, padded `0 4px` and inverted only when current) and who share the first line with the action pushed to its end, and what runs on its own line below in the note size (12px/1.45 sans, in ink), wrapping anywhere. Entries are `6px 0` apart over 1px rules with a 2px gap between their lines; there is no rule above the first and no padding before the first or after the last, so the list starts where a body's content starts.
|
|
1167
|
-
- **LineDiff** → `div.diff-wrap > table.diff > tr.add | tr.del | tr.same | tr.fold`: a Myers line diff (jsdiff's `diff`, a runtime dependency) in a 2px ink box on paper that scrolls sideways; 12px/1.6 system-mono lines, each with two 10px pixel-face line numbers behind a 1px rule, a sign column, and the line. Added lines are Head Tint behind an ink `+`; deleted lines muted behind a red `−`; unchanged runs beyond two lines of context fold into a head-tinted row labelled in 10px uppercase On Head. On phones lines wrap. `LineDiff.patchView` draws the same table from one file's unified diff, the lines between its hunks folded into the counted row;
|
|
1167
|
+
- **LineDiff** → `div.diff-wrap > table.diff > tr.add | tr.del | tr.same | tr.fold`: a Myers line diff (jsdiff's `diff`, a runtime dependency) in a 2px ink box on paper that scrolls sideways; 12px/1.6 system-mono lines, each with two 10px pixel-face line numbers behind a 1px rule, a sign column, and the line. Added lines are Head Tint behind an ink `+`; deleted lines muted behind a red `−`; unchanged runs beyond two lines of context fold into a head-tinted row labelled in 10px uppercase On Head. On phones lines wrap. `LineDiff.patchView` draws the same table from one file's unified diff, the lines between its hunks folded into the counted row. What git's header says prints as one note line, above the table or in its place: a rename (its path unquoted), a mode change, a new or deleted empty file, a binary change; a file whose type changed (a file that became a symlink) is one note over a table for each side, named for its side, the note and tables in a `div.stack.gap-s`. A lone table or note is drawn bare, so a flush window's body still meets it. Text that is not a diff, or holds several files, prints a note line that says it cannot be shown as lines. `LineDiff.describePatch` gives the notes, for a caller that prints them itself.
|
|
1168
1168
|
|
|
1169
1169
|
### Meters and cells
|
|
1170
1170
|
|
|
@@ -24,6 +24,9 @@ export type ViewConfig<Message> = Readonly<{
|
|
|
24
24
|
isBoxed?: boolean;
|
|
25
25
|
/** Code and identifiers: the system monospace. */
|
|
26
26
|
isMono?: boolean;
|
|
27
|
+
/** The label is for screen readers only (`.sr-only`): a field whose place says what it is,
|
|
28
|
+
* such as a browser's address bar. It still names the input. */
|
|
29
|
+
isLabelHidden?: boolean;
|
|
27
30
|
/** The input's type (`"date"` for the browser's date control, always boxed); text when not
|
|
28
31
|
* given. */
|
|
29
32
|
type?: string;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"field.d.ts","sourceRoot":"","sources":["../../../src/field/field.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEzE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAM9D,wFAAwF;AACxF,eAAO,MAAM,SAAS,GAAI,OAAO,MAC3B,MAAM,SACH,MAAM,GAAG,SAAS,KACtB,WAAW,CAAC,OAAO,CAAC,KACtB,aAAa,CAAC,IAAI,CACmE,CAAC;AAEzF;;+EAE+E;AAC/E,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAClC,MAAM,SACH,MAAM,GAAG,SAAS,WAChB,OAAO,KACb,WAAW,CAAC,OAAO,CAAC,KACtB,aAAa,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAGqD,CAAC;AAIjG,4DAA4D;AAC5D,MAAM,MAAM,UAAU,CAAC,OAAO,IAAI,QAAQ,CAAC;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IACpC,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;uCAEmC;IACnC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,yFAAyF;IACzF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,0FAA0F;IAC1F,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kDAAkD;IAClD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;iBACa;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;iBACa;IACb,eAAe,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;CACvC,CAAC,CAAC;AAEH;;;6EAG6E;AAC7E,eAAO,MAAM,IAAI,GAAI,OAAO,UAAU,UAAU,CAAC,OAAO,CAAC,KAAK,WAAW,CAAC,OAAO,CAAC,KAAG,
|
|
1
|
+
{"version":3,"file":"field.d.ts","sourceRoot":"","sources":["../../../src/field/field.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,iBAAiB,EAAE,MAAM,cAAc,CAAC;AAEzE,OAAO,KAAK,EAAE,UAAU,EAAE,MAAM,6BAA6B,CAAC;AAM9D,wFAAwF;AACxF,eAAO,MAAM,SAAS,GAAI,OAAO,MAC3B,MAAM,SACH,MAAM,GAAG,SAAS,KACtB,WAAW,CAAC,OAAO,CAAC,KACtB,aAAa,CAAC,IAAI,CACmE,CAAC;AAEzF;;+EAE+E;AAC/E,eAAO,MAAM,gBAAgB,GAAI,OAAO,MAClC,MAAM,SACH,MAAM,GAAG,SAAS,WAChB,OAAO,KACb,WAAW,CAAC,OAAO,CAAC,KACtB,aAAa,CAAC,iBAAiB,CAAC,OAAO,CAAC,CAGqD,CAAC;AAIjG,4DAA4D;AAC5D,MAAM,MAAM,UAAU,CAAC,OAAO,IAAI,QAAQ,CAAC;IACzC,EAAE,EAAE,MAAM,CAAC;IACX,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;IACd,OAAO,EAAE,CAAC,KAAK,EAAE,MAAM,KAAK,OAAO,CAAC;IACpC,yCAAyC;IACzC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;;uCAEmC;IACnC,KAAK,CAAC,EAAE,MAAM,GAAG,SAAS,CAAC;IAC3B,yFAAyF;IACzF,UAAU,CAAC,EAAE,OAAO,CAAC;IACrB,0FAA0F;IAC1F,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,kDAAkD;IAClD,MAAM,CAAC,EAAE,OAAO,CAAC;IACjB;qEACiE;IACjE,aAAa,CAAC,EAAE,OAAO,CAAC;IACxB;iBACa;IACb,IAAI,CAAC,EAAE,MAAM,CAAC;IACd;iBACa;IACb,eAAe,CAAC,EAAE,UAAU,CAAC,OAAO,CAAC,CAAC;CACvC,CAAC,CAAC;AAEH;;;6EAG6E;AAC7E,eAAO,MAAM,IAAI,GAAI,OAAO,UAAU,UAAU,CAAC,OAAO,CAAC,KAAK,WAAW,CAAC,OAAO,CAAC,KAAG,IA6CpF,CAAC"}
|
package/dist/src/field/field.js
CHANGED
|
@@ -15,7 +15,7 @@ export const describedByError = (id, error, hasHelp, h) => __foldkitBrandViewRes
|
|
|
15
15
|
* `aria-invalid` and describes it before the help, and `isRequired` sets `aria-required`.
|
|
16
16
|
* The input is boxless, a dotted baseline that turns solid ink on focus. */
|
|
17
17
|
export const view = (config, h) => {
|
|
18
|
-
const { id, label, value, onInput, help, error, isRequired = false, isBoxed = false, isMono = false, type, inputAttributes = [], } = config;
|
|
18
|
+
const { id, label, value, onInput, help, error, isRequired = false, isBoxed = false, isMono = false, isLabelHidden = false, type, inputAttributes = [], } = config;
|
|
19
19
|
return __foldkitBrandViewResult((UiInput.view({
|
|
20
20
|
id,
|
|
21
21
|
value,
|
|
@@ -24,7 +24,7 @@ export const view = (config, h) => {
|
|
|
24
24
|
...(type !== undefined && { type }),
|
|
25
25
|
hasDescription: help !== undefined,
|
|
26
26
|
toView: (groups) => __foldkitBrandViewResult((h.div([h.Class(`field${isBoxed || type === "date" ? " boxed" : ""}${isMono ? " mono" : ""}`)], [
|
|
27
|
-
h.label(groups.label, [label]),
|
|
27
|
+
h.label([...groups.label, ...(isLabelHidden ? [h.Class("sr-only")] : [])], [label]),
|
|
28
28
|
// NOTE: @foldkit/ui gap: Input has no required option, so the wrapper says it.
|
|
29
29
|
h.input([
|
|
30
30
|
...inputAttributes,
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lineDiff/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAChD,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/lineDiff/index.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,SAAS,EAAE,IAAI,EAAE,MAAM,eAAe,CAAC;AAChD,YAAY,EAAE,eAAe,EAAE,UAAU,EAAE,MAAM,eAAe,CAAC;AACjE,OAAO,EAAE,aAAa,EAAE,MAAM,YAAY,CAAC;AAC3C,YAAY,EAAE,gBAAgB,EAAE,MAAM,YAAY,CAAC"}
|
|
@@ -11,7 +11,8 @@ export type ViewConfig = Readonly<{
|
|
|
11
11
|
}>;
|
|
12
12
|
/** Configuration for rendering a unified diff with `patchView`. */
|
|
13
13
|
export type PatchViewConfig = Readonly<{
|
|
14
|
-
/** One file's unified diff, as `diff -u` or `git diff -- <path>` writes it
|
|
14
|
+
/** One file's unified diff, as `diff -u` or `git diff -- <path>` writes it, its git header
|
|
15
|
+
* included. */
|
|
15
16
|
patch: string;
|
|
16
17
|
/** Names the two sides for screen readers: `"Version 3 against version 4"`. */
|
|
17
18
|
label: string;
|
|
@@ -23,8 +24,11 @@ export declare const view: <Message>(config: ViewConfig, h: HtmlBuilder<Message>
|
|
|
23
24
|
/** A unified diff the caller already has (from a server, or `git diff -- <path>`) as `view`'s
|
|
24
25
|
* table: its hunks' lines, numbered from their headers, and the unchanged lines between hunks
|
|
25
26
|
* folded into a counted row. Lines past the last hunk are not in the patch, so nothing counts
|
|
26
|
-
* them.
|
|
27
|
-
*
|
|
28
|
-
*
|
|
27
|
+
* them. What git's header says beyond the lines prints as a note line above the table, or in
|
|
28
|
+
* its place: a rename, a mode change, an empty or binary file (`describePatch` has the
|
|
29
|
+
* sentences). A file whose type changed (a file that became a symlink) is two sections, each
|
|
30
|
+
* its own table under one note, named for its side (`"<label>, as it was"`). An empty patch
|
|
31
|
+
* says there is no difference, as `view` does; text that is not a diff, or holds several
|
|
32
|
+
* files, says it cannot be shown as lines. */
|
|
29
33
|
export declare const patchView: <Message>(config: PatchViewConfig, h: HtmlBuilder<Message>) => Html;
|
|
30
34
|
//# sourceMappingURL=lineDiff.d.ts.map
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"lineDiff.d.ts","sourceRoot":"","sources":["../../../src/lineDiff/lineDiff.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;
|
|
1
|
+
{"version":3,"file":"lineDiff.d.ts","sourceRoot":"","sources":["../../../src/lineDiff/lineDiff.ts"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAWtD,2DAA2D;AAC3D,MAAM,MAAM,UAAU,GAAG,QAAQ,CAAC;IAChC,MAAM,EAAE,MAAM,CAAC;IACf,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,KAAK,EAAE,MAAM,CAAC;IACd;mBACe;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB,CAAC,CAAC;AAEH,mEAAmE;AACnE,MAAM,MAAM,eAAe,GAAG,QAAQ,CAAC;IACrC;oBACgB;IAChB,KAAK,EAAE,MAAM,CAAC;IACd,+EAA+E;IAC/E,KAAK,EAAE,MAAM,CAAC;CACf,CAAC,CAAC;AAyEH;;kFAEkF;AAClF,eAAO,MAAM,IAAI,GAAI,OAAO,UAAU,UAAU,KAAK,WAAW,CAAC,OAAO,CAAC,KAAG,IAO3E,CAAC;AAEF;;;;;;;;+CAQ+C;AAC/C,eAAO,MAAM,SAAS,GAAI,OAAO,UAAU,eAAe,KAAK,WAAW,CAAC,OAAO,CAAC,KAAG,IAoBrF,CAAC"}
|
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
import { brandViewResult as __foldkitBrandViewResult } from 'foldkit/brand'
|
|
2
|
-
import { Array, Option
|
|
2
|
+
import { Array, Option } from "effect";
|
|
3
3
|
import { LOCALE } from "../locale/locale.js";
|
|
4
|
-
import { DiffRow, diffRowsOf,
|
|
4
|
+
import { DiffRow, diffRowsOf, foldUnchanged, hasChanges } from "./diff.js";
|
|
5
|
+
import { SAME_NOTE, readPatch } from "./patch.js";
|
|
5
6
|
// VIEW
|
|
6
7
|
const DEFAULT_CONTEXT = 2;
|
|
7
8
|
const COLUMN_COUNT = 4;
|
|
@@ -29,7 +30,6 @@ const rowView = (row, h) => __foldkitBrandViewResult((DiffRow.match(row, {
|
|
|
29
30
|
Fold: ({ count, firstOldNumber }) => __foldkitBrandViewResult((foldView(count, firstOldNumber, h)), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/lineDiff.js#Fold"),
|
|
30
31
|
})), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/lineDiff.js#rowView");
|
|
31
32
|
const noteView = (note, h) => __foldkitBrandViewResult((h.p([h.Class("note-line")], [note])), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/lineDiff.js#noteView");
|
|
32
|
-
const SAME_NOTE = "No difference: both sides hold the same text.";
|
|
33
33
|
const tableView = (rows, label, h) => __foldkitBrandViewResult((h.div([h.Class("diff-wrap")], [
|
|
34
34
|
h.table([h.AriaLabel(label), h.Class("diff")], [
|
|
35
35
|
h.tbody([], Array.map(rows, (row) => __foldkitBrandViewResult((rowView(row, h)), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/lineDiff.js#anonymous"))),
|
|
@@ -48,18 +48,22 @@ export const view = (config, h) => {
|
|
|
48
48
|
/** A unified diff the caller already has (from a server, or `git diff -- <path>`) as `view`'s
|
|
49
49
|
* table: its hunks' lines, numbered from their headers, and the unchanged lines between hunks
|
|
50
50
|
* folded into a counted row. Lines past the last hunk are not in the patch, so nothing counts
|
|
51
|
-
* them.
|
|
52
|
-
*
|
|
53
|
-
*
|
|
51
|
+
* them. What git's header says beyond the lines prints as a note line above the table, or in
|
|
52
|
+
* its place: a rename, a mode change, an empty or binary file (`describePatch` has the
|
|
53
|
+
* sentences). A file whose type changed (a file that became a symlink) is two sections, each
|
|
54
|
+
* its own table under one note, named for its side (`"<label>, as it was"`). An empty patch
|
|
55
|
+
* says there is no difference, as `view` does; text that is not a diff, or holds several
|
|
56
|
+
* files, says it cannot be shown as lines. */
|
|
54
57
|
export const patchView = (config, h) => {
|
|
55
58
|
const { patch, label } = config;
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
59
|
+
const { notes, tables } = readPatch(patch);
|
|
60
|
+
const parts = [
|
|
61
|
+
...(Array.isReadonlyArrayNonEmpty(notes) ? [noteView(Array.join(notes, " "), h)] : []),
|
|
62
|
+
...Array.map(tables, ({ rows, maybeSide }) => __foldkitBrandViewResult((tableView(rows, Option.match(maybeSide, { onNone: () => __foldkitBrandViewResult((label), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/lineDiff.js#onNone"), onSome: (side) => __foldkitBrandViewResult((`${label}, ${side}`), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/lineDiff.js#onSome") }), h)), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/lineDiff.js#anonymous~2")),
|
|
63
|
+
];
|
|
64
|
+
// a lone table or note is drawn bare, so a flush window's body still meets its table
|
|
65
|
+
return __foldkitBrandViewResult((Array.match(parts, {
|
|
66
|
+
onEmpty: () => __foldkitBrandViewResult((noteView(SAME_NOTE, h)), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/lineDiff.js#onEmpty"),
|
|
67
|
+
onNonEmpty: ([part, ...others]) => __foldkitBrandViewResult((Array.isArrayEmpty(others) ? part : h.div([h.Class("stack gap-s")], parts)), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/lineDiff.js#onNonEmpty"),
|
|
68
|
+
})), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/lineDiff.js#patchView");
|
|
65
69
|
};
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import { Option } from "effect";
|
|
2
|
+
import { type DiffRow } from "./diff.ts";
|
|
3
|
+
/** The name a path in git's header stands for: git C-quotes a name with unusual bytes
|
|
4
|
+
* (`"caf\303\251.txt"`), and leaves any other as it is. */
|
|
5
|
+
export declare const unquoted: (path: string) => string;
|
|
6
|
+
/** What a diff of two same texts says. */
|
|
7
|
+
export declare const SAME_NOTE = "No difference: both sides hold the same text.";
|
|
8
|
+
/** A table of the patch's lines; `maybeSide` says which side of a type change it is. */
|
|
9
|
+
type Table = Readonly<{
|
|
10
|
+
rows: ReadonlyArray<DiffRow>;
|
|
11
|
+
maybeSide: Option.Option<string>;
|
|
12
|
+
}>;
|
|
13
|
+
/** What `patchView` draws: its notes as a line, then its tables. */
|
|
14
|
+
export type PatchReading = Readonly<{
|
|
15
|
+
maybeRenamedFrom: Option.Option<string>;
|
|
16
|
+
notes: ReadonlyArray<string>;
|
|
17
|
+
tables: ReadonlyArray<Table>;
|
|
18
|
+
}>;
|
|
19
|
+
/** What `patchView` makes of a patch: the notes it prints and the tables it draws. */
|
|
20
|
+
export declare const readPatch: (patch: string) => PatchReading;
|
|
21
|
+
/** What a patch says beyond its lines, as `patchView` prints it. */
|
|
22
|
+
export type PatchDescription = Readonly<{
|
|
23
|
+
/** The path a rename left, unquoted: for a heading such as `old.txt → new.txt`. */
|
|
24
|
+
maybeRenamedFrom: Option.Option<string>;
|
|
25
|
+
/** The sentences `patchView` prints above the patch's lines, or in their place: a rename,
|
|
26
|
+
* a mode change, a type change, an empty or binary file, no difference, or a patch it
|
|
27
|
+
* cannot show as lines. */
|
|
28
|
+
notes: ReadonlyArray<string>;
|
|
29
|
+
}>;
|
|
30
|
+
/** What `patchView` says about a patch from `git diff`, read from its header: the notes it
|
|
31
|
+
* prints (`["Renamed from old.txt."]`), for a caller that prints them itself, and the path a
|
|
32
|
+
* rename left. */
|
|
33
|
+
export declare const describePatch: (patch: string) => PatchDescription;
|
|
34
|
+
export {};
|
|
35
|
+
//# sourceMappingURL=patch.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"patch.d.ts","sourceRoot":"","sources":["../../../src/lineDiff/patch.ts"],"names":[],"mappings":"AAAA,OAAO,EAAS,MAAM,EAAgB,MAAM,QAAQ,CAAC;AAErD,OAAO,EAAE,KAAK,OAAO,EAAmB,MAAM,WAAW,CAAC;AAgC1D;4DAC4D;AAC5D,eAAO,MAAM,QAAQ,SAAU,MAAM,KAAG,MAO9B,CAAC;AAsCX,0CAA0C;AAC1C,eAAO,MAAM,SAAS,kDAAkD,CAAC;AAIzE,wFAAwF;AACxF,KAAK,KAAK,GAAG,QAAQ,CAAC;IAAE,IAAI,EAAE,aAAa,CAAC,OAAO,CAAC,CAAC;IAAC,SAAS,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAA;CAAE,CAAC,CAAC;AAE1F,oEAAoE;AACpE,MAAM,MAAM,YAAY,GAAG,QAAQ,CAAC;IAClC,gBAAgB,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACxC,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;IAC7B,MAAM,EAAE,aAAa,CAAC,KAAK,CAAC,CAAC;CAC9B,CAAC,CAAC;AAoFH,sFAAsF;AACtF,eAAO,MAAM,SAAS,UAAW,MAAM,KAAG,YAiBzC,CAAC;AAIF,oEAAoE;AACpE,MAAM,MAAM,gBAAgB,GAAG,QAAQ,CAAC;IACtC,mFAAmF;IACnF,gBAAgB,EAAE,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;IACxC;;gCAE4B;IAC5B,KAAK,EAAE,aAAa,CAAC,MAAM,CAAC,CAAC;CAC9B,CAAC,CAAC;AAEH;;mBAEmB;AACnB,eAAO,MAAM,aAAa,UAAW,MAAM,KAAG,gBAI7C,CAAC"}
|
|
@@ -0,0 +1,146 @@
|
|
|
1
|
+
import { brandViewResult as __foldkitBrandViewResult } from 'foldkit/brand'
|
|
2
|
+
import { Array, Option, String, pipe } from "effect";
|
|
3
|
+
import { diffRowsOfPatch } from "./diff.js";
|
|
4
|
+
// QUOTED PATHS
|
|
5
|
+
const ESCAPED_BYTES = {
|
|
6
|
+
a: 7,
|
|
7
|
+
b: 8,
|
|
8
|
+
t: 9,
|
|
9
|
+
n: 10,
|
|
10
|
+
v: 11,
|
|
11
|
+
f: 12,
|
|
12
|
+
r: 13,
|
|
13
|
+
};
|
|
14
|
+
// an octal escape is one byte of the name's UTF-8, so the bytes are gathered before decoding
|
|
15
|
+
const QUOTED_PART = /\\([0-7]{3})|\\(.)|[^\\]+/gu;
|
|
16
|
+
const bytesOf = (text) => __foldkitBrandViewResult(([...new TextEncoder().encode(text)]), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#bytesOf");
|
|
17
|
+
const bytesOfPart = ([part, octal, escaped]) => {
|
|
18
|
+
if (octal !== undefined) {
|
|
19
|
+
return __foldkitBrandViewResult(([Number.parseInt(octal, 8)]), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#bytesOfPart");
|
|
20
|
+
}
|
|
21
|
+
else if (escaped !== undefined) {
|
|
22
|
+
return __foldkitBrandViewResult((Option.match(Option.fromUndefinedOr(ESCAPED_BYTES[escaped]), {
|
|
23
|
+
onNone: () => __foldkitBrandViewResult((bytesOf(escaped)), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#onNone"),
|
|
24
|
+
onSome: (byte) => __foldkitBrandViewResult(([byte]), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#onSome"),
|
|
25
|
+
})), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#bytesOfPart");
|
|
26
|
+
}
|
|
27
|
+
else {
|
|
28
|
+
return __foldkitBrandViewResult((bytesOf(part)), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#bytesOfPart");
|
|
29
|
+
}
|
|
30
|
+
};
|
|
31
|
+
/** The name a path in git's header stands for: git C-quotes a name with unusual bytes
|
|
32
|
+
* (`"caf\303\251.txt"`), and leaves any other as it is. */
|
|
33
|
+
export const unquoted = (path) => __foldkitBrandViewResult((path.length >= 2 && path.startsWith('"') && path.endsWith('"')
|
|
34
|
+
? new TextDecoder().decode(Uint8Array.from(Array.flatMap(Array.fromIterable(path.slice(1, -1).matchAll(QUOTED_PART)), bytesOfPart)))
|
|
35
|
+
: path), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#unquoted");
|
|
36
|
+
// HEADER
|
|
37
|
+
const GIT_SECTION = "diff --git ";
|
|
38
|
+
// the header is what comes before the first hunk, so a changed line that reads like a header
|
|
39
|
+
// line ("+rename from x") is never taken for one
|
|
40
|
+
const headOf = (section) => __foldkitBrandViewResult((Array.takeWhile(section.split("\n"), (line) => __foldkitBrandViewResult((!line.startsWith("@@") && !line.startsWith("--- ")), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#anonymous"))), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#headOf");
|
|
41
|
+
const valueAfter = (head, prefix) => __foldkitBrandViewResult((pipe(Array.findFirst(head, String.startsWith(prefix)), Option.map((line) => __foldkitBrandViewResult((line.slice(prefix.length)), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#anonymous~2")))), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#valueAfter");
|
|
42
|
+
const hasLine = (head, prefix) => __foldkitBrandViewResult((Option.isSome(valueAfter(head, prefix))), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#hasLine");
|
|
43
|
+
const hasHunk = (section) => __foldkitBrandViewResult((Array.some(section.split("\n"), String.startsWith("@@ "))), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#hasHunk");
|
|
44
|
+
// git writes one section per file, after whatever precedes the first (a commit's message, a
|
|
45
|
+
// mail's head); a patch without a git header is one section
|
|
46
|
+
const sectionsOf = (patch) => {
|
|
47
|
+
const [first = "", ...others] = patch.split(/(?=^diff --git )/mu);
|
|
48
|
+
return __foldkitBrandViewResult((Array.isArrayNonEmpty(others) && !first.startsWith(GIT_SECTION)
|
|
49
|
+
? others
|
|
50
|
+
: [first, ...others]), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#sectionsOf");
|
|
51
|
+
};
|
|
52
|
+
// READING
|
|
53
|
+
/** What a diff of two same texts says. */
|
|
54
|
+
export const SAME_NOTE = "No difference: both sides hold the same text.";
|
|
55
|
+
const UNREADABLE_NOTE = "This diff cannot be shown as lines.";
|
|
56
|
+
const BINARY_NOTE = "A binary file: its change has no lines.";
|
|
57
|
+
const unreadable = {
|
|
58
|
+
maybeRenamedFrom: Option.none(),
|
|
59
|
+
notes: [UNREADABLE_NOTE],
|
|
60
|
+
tables: [],
|
|
61
|
+
};
|
|
62
|
+
const lineNote = (head, hasRows) => {
|
|
63
|
+
if (hasLine(head, "Binary files ") || hasLine(head, "GIT binary patch")) {
|
|
64
|
+
return __foldkitBrandViewResult((Option.some(BINARY_NOTE)), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#lineNote");
|
|
65
|
+
}
|
|
66
|
+
else if (hasRows) {
|
|
67
|
+
return __foldkitBrandViewResult((Option.none()), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#lineNote");
|
|
68
|
+
}
|
|
69
|
+
else if (hasLine(head, "new file mode ")) {
|
|
70
|
+
return __foldkitBrandViewResult((Option.some("New empty file.")), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#lineNote");
|
|
71
|
+
}
|
|
72
|
+
else if (hasLine(head, "deleted file mode ")) {
|
|
73
|
+
return __foldkitBrandViewResult((Option.some("Deleted empty file.")), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#lineNote");
|
|
74
|
+
}
|
|
75
|
+
else {
|
|
76
|
+
return __foldkitBrandViewResult((Option.none()), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#lineNote");
|
|
77
|
+
}
|
|
78
|
+
};
|
|
79
|
+
const readSection = (section) => {
|
|
80
|
+
const head = headOf(section);
|
|
81
|
+
const maybeRows = diffRowsOfPatch(section);
|
|
82
|
+
const maybeRenamedFrom = Option.map(valueAfter(head, "rename from "), unquoted);
|
|
83
|
+
const notes = [
|
|
84
|
+
...Option.toArray(Option.map(maybeRenamedFrom, (from) => __foldkitBrandViewResult((`Renamed from ${from}.`), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#anonymous~3"))),
|
|
85
|
+
...Option.toArray(Option.map(Option.all({ before: valueAfter(head, "old mode "), after: valueAfter(head, "new mode ") }), ({ before, after }) => __foldkitBrandViewResult((`Mode ${before} → ${after}.`), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#anonymous~4"))),
|
|
86
|
+
...Option.toArray(lineNote(head, Option.isSome(maybeRows))),
|
|
87
|
+
];
|
|
88
|
+
const isUnread = Option.isNone(maybeRows) && (hasHunk(section) || Array.isArrayEmpty(notes));
|
|
89
|
+
return __foldkitBrandViewResult((isUnread
|
|
90
|
+
? unreadable
|
|
91
|
+
: {
|
|
92
|
+
maybeRenamedFrom,
|
|
93
|
+
notes,
|
|
94
|
+
tables: Option.toArray(Option.map(maybeRows, (rows) => (__foldkitBrandViewResult(({ rows, maybeSide: Option.none() }), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#anonymous~5")))),
|
|
95
|
+
}), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#readSection");
|
|
96
|
+
};
|
|
97
|
+
const KINDS = {
|
|
98
|
+
"100644": "a file",
|
|
99
|
+
"100755": "an executable file",
|
|
100
|
+
"120000": "a symlink",
|
|
101
|
+
"160000": "a submodule",
|
|
102
|
+
};
|
|
103
|
+
const kindOf = (mode) => __foldkitBrandViewResult((KINDS[mode] ?? `mode ${mode}`), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#kindOf");
|
|
104
|
+
const sideTable = (section, side) => __foldkitBrandViewResult((Option.toArray(Option.map(diffRowsOfPatch(section), (rows) => (__foldkitBrandViewResult(({ rows, maybeSide: Option.some(side) }), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#anonymous~6"))))), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#sideTable");
|
|
105
|
+
// a file that became a symlink (or any other kind) is two sections for one path: the old
|
|
106
|
+
// kind deleted, then the new one added
|
|
107
|
+
const readTypeChange = (gone, added) => {
|
|
108
|
+
const [goneHead, addedHead] = [headOf(gone), headOf(added)];
|
|
109
|
+
return __foldkitBrandViewResult((Option.flatMap(Option.all({
|
|
110
|
+
before: valueAfter(goneHead, "deleted file mode "),
|
|
111
|
+
after: valueAfter(addedHead, "new file mode "),
|
|
112
|
+
}), ({ before, after }) => __foldkitBrandViewResult((goneHead[0] === addedHead[0]
|
|
113
|
+
? Option.some({
|
|
114
|
+
maybeRenamedFrom: Option.none(),
|
|
115
|
+
notes: [`It was ${kindOf(before)} and is now ${kindOf(after)}.`],
|
|
116
|
+
tables: [...sideTable(gone, "as it was"), ...sideTable(added, "as it is")],
|
|
117
|
+
})
|
|
118
|
+
: Option.none()), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#anonymous~7"))), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#readTypeChange");
|
|
119
|
+
};
|
|
120
|
+
/** What `patchView` makes of a patch: the notes it prints and the tables it draws. */
|
|
121
|
+
export const readPatch = (patch) => {
|
|
122
|
+
const text = patch.replaceAll("\r\n", "\n");
|
|
123
|
+
if (String.isEmpty(String.trim(text))) {
|
|
124
|
+
return __foldkitBrandViewResult(({ maybeRenamedFrom: Option.none(), notes: [SAME_NOTE], tables: [] }), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#readPatch");
|
|
125
|
+
}
|
|
126
|
+
else {
|
|
127
|
+
const sections = sectionsOf(text);
|
|
128
|
+
const [first, second, ...rest] = sections;
|
|
129
|
+
if (first !== undefined && second === undefined) {
|
|
130
|
+
return __foldkitBrandViewResult((readSection(first)), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#readPatch");
|
|
131
|
+
}
|
|
132
|
+
else if (first !== undefined && second !== undefined && Array.isArrayEmpty(rest)) {
|
|
133
|
+
return __foldkitBrandViewResult((Option.getOrElse(readTypeChange(first, second), () => __foldkitBrandViewResult((unreadable), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#anonymous~8"))), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#readPatch");
|
|
134
|
+
}
|
|
135
|
+
else {
|
|
136
|
+
return __foldkitBrandViewResult((unreadable), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#readPatch");
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
};
|
|
140
|
+
/** What `patchView` says about a patch from `git diff`, read from its header: the notes it
|
|
141
|
+
* prints (`["Renamed from old.txt."]`), for a caller that prints them itself, and the path a
|
|
142
|
+
* rename left. */
|
|
143
|
+
export const describePatch = (patch) => {
|
|
144
|
+
const { maybeRenamedFrom, notes } = readPatch(patch);
|
|
145
|
+
return __foldkitBrandViewResult(({ maybeRenamedFrom, notes }), "@gjermundgaraba-designs/pixel-paper/src/lineDiff/patch.js#describePatch");
|
|
146
|
+
};
|
package/dist/src/menu/menu.d.ts
CHANGED
|
@@ -76,6 +76,8 @@ export declare const update: (model: Model, message: Message) => Readonly<{
|
|
|
76
76
|
};
|
|
77
77
|
commands?: Update.Commands<{
|
|
78
78
|
readonly _tag: "CompletedFocus";
|
|
79
|
+
} | {
|
|
80
|
+
readonly _tag: "PressedEscape";
|
|
79
81
|
} | {
|
|
80
82
|
readonly _tag: "ClickedToggle";
|
|
81
83
|
} | {
|
|
@@ -84,8 +86,6 @@ export declare const update: (model: Model, message: Message) => Readonly<{
|
|
|
84
86
|
readonly _tag: "CompletedPlaceSlip";
|
|
85
87
|
} | {
|
|
86
88
|
readonly _tag: "LeftSlip";
|
|
87
|
-
} | {
|
|
88
|
-
readonly _tag: "PressedEscape";
|
|
89
89
|
} | {
|
|
90
90
|
readonly _tag: "PressedTab";
|
|
91
91
|
} | {
|
|
@@ -125,11 +125,11 @@ export declare const BindSlip: Mount.MountDefinitionWithArgs<"BindSlip", {
|
|
|
125
125
|
isEnd: Schema.Boolean;
|
|
126
126
|
edge: Schema.Literals<readonly ["First", "Last"]>;
|
|
127
127
|
}, {
|
|
128
|
+
readonly _tag: "PressedEscape";
|
|
129
|
+
} | {
|
|
128
130
|
readonly _tag: "CompletedPlaceSlip";
|
|
129
131
|
} | {
|
|
130
132
|
readonly _tag: "LeftSlip";
|
|
131
|
-
} | {
|
|
132
|
-
readonly _tag: "PressedEscape";
|
|
133
133
|
} | {
|
|
134
134
|
readonly _tag: "PressedTab";
|
|
135
135
|
}>;
|
|
@@ -175,6 +175,8 @@ export declare const view: import("foldkit/submodel").View<{
|
|
|
175
175
|
readonly edge: "First" | "Last";
|
|
176
176
|
}, {
|
|
177
177
|
readonly _tag: "CompletedFocus";
|
|
178
|
+
} | {
|
|
179
|
+
readonly _tag: "PressedEscape";
|
|
178
180
|
} | {
|
|
179
181
|
readonly _tag: "ClickedToggle";
|
|
180
182
|
} | {
|
|
@@ -183,8 +185,6 @@ export declare const view: import("foldkit/submodel").View<{
|
|
|
183
185
|
readonly _tag: "CompletedPlaceSlip";
|
|
184
186
|
} | {
|
|
185
187
|
readonly _tag: "LeftSlip";
|
|
186
|
-
} | {
|
|
187
|
-
readonly _tag: "PressedEscape";
|
|
188
188
|
} | {
|
|
189
189
|
readonly _tag: "PressedTab";
|
|
190
190
|
} | {
|
|
@@ -245,8 +245,6 @@ export declare const update: (model: Model, message: Message) => Readonly<{
|
|
|
245
245
|
}>;
|
|
246
246
|
};
|
|
247
247
|
commands?: Update.Commands<{
|
|
248
|
-
readonly _tag: "PressedEscape";
|
|
249
|
-
} | {
|
|
250
248
|
readonly _tag: "CancelledPointer";
|
|
251
249
|
} | {
|
|
252
250
|
readonly _tag: "CompletedAnnounceReorder";
|
|
@@ -255,9 +253,6 @@ export declare const update: (model: Model, message: Message) => Readonly<{
|
|
|
255
253
|
} | {
|
|
256
254
|
readonly _tag: "GotDragAndDropMessage";
|
|
257
255
|
readonly message: {
|
|
258
|
-
readonly _tag: "PressedArrowKey";
|
|
259
|
-
readonly direction: "Down" | "Left" | "NextContainer" | "PreviousContainer" | "Right" | "Up";
|
|
260
|
-
} | {
|
|
261
256
|
readonly _tag: "ActivatedKeyboardDrag";
|
|
262
257
|
readonly itemId: string;
|
|
263
258
|
readonly containerId: string;
|
|
@@ -284,6 +279,9 @@ export declare const update: (model: Model, message: Message) => Readonly<{
|
|
|
284
279
|
readonly containerId: string;
|
|
285
280
|
readonly index: number;
|
|
286
281
|
}>;
|
|
282
|
+
} | {
|
|
283
|
+
readonly _tag: "PressedArrowKey";
|
|
284
|
+
readonly direction: "Down" | "Left" | "NextContainer" | "PreviousContainer" | "Right" | "Up";
|
|
287
285
|
} | {
|
|
288
286
|
readonly _tag: "PressedDraggable";
|
|
289
287
|
readonly itemId: string;
|
|
@@ -301,6 +299,8 @@ export declare const update: (model: Model, message: Message) => Readonly<{
|
|
|
301
299
|
readonly clientX: number;
|
|
302
300
|
readonly clientY: number;
|
|
303
301
|
readonly maybeIndex: Option.Option<number>;
|
|
302
|
+
} | {
|
|
303
|
+
readonly _tag: "PressedEscape";
|
|
304
304
|
} | {
|
|
305
305
|
readonly _tag: "PressedGripKey";
|
|
306
306
|
readonly itemId: string;
|
|
@@ -413,8 +413,6 @@ export declare const subscriptions: <Prefix extends string>(keyPrefix: Prefix) =
|
|
|
413
413
|
}>;
|
|
414
414
|
}>;
|
|
415
415
|
}, {
|
|
416
|
-
readonly _tag: "PressedEscape";
|
|
417
|
-
} | {
|
|
418
416
|
readonly _tag: "CancelledPointer";
|
|
419
417
|
} | {
|
|
420
418
|
readonly _tag: "CompletedAnnounceReorder";
|
|
@@ -423,9 +421,6 @@ export declare const subscriptions: <Prefix extends string>(keyPrefix: Prefix) =
|
|
|
423
421
|
} | {
|
|
424
422
|
readonly _tag: "GotDragAndDropMessage";
|
|
425
423
|
readonly message: {
|
|
426
|
-
readonly _tag: "PressedArrowKey";
|
|
427
|
-
readonly direction: "Down" | "Left" | "NextContainer" | "PreviousContainer" | "Right" | "Up";
|
|
428
|
-
} | {
|
|
429
424
|
readonly _tag: "ActivatedKeyboardDrag";
|
|
430
425
|
readonly itemId: string;
|
|
431
426
|
readonly containerId: string;
|
|
@@ -452,6 +447,9 @@ export declare const subscriptions: <Prefix extends string>(keyPrefix: Prefix) =
|
|
|
452
447
|
readonly containerId: string;
|
|
453
448
|
readonly index: number;
|
|
454
449
|
}>;
|
|
450
|
+
} | {
|
|
451
|
+
readonly _tag: "PressedArrowKey";
|
|
452
|
+
readonly direction: "Down" | "Left" | "NextContainer" | "PreviousContainer" | "Right" | "Up";
|
|
455
453
|
} | {
|
|
456
454
|
readonly _tag: "PressedDraggable";
|
|
457
455
|
readonly itemId: string;
|
|
@@ -469,6 +467,8 @@ export declare const subscriptions: <Prefix extends string>(keyPrefix: Prefix) =
|
|
|
469
467
|
readonly clientX: number;
|
|
470
468
|
readonly clientY: number;
|
|
471
469
|
readonly maybeIndex: Option.Option<number>;
|
|
470
|
+
} | {
|
|
471
|
+
readonly _tag: "PressedEscape";
|
|
472
472
|
} | {
|
|
473
473
|
readonly _tag: "PressedGripKey";
|
|
474
474
|
readonly itemId: string;
|
|
@@ -559,8 +559,6 @@ export declare const subscriptions: <Prefix extends string>(keyPrefix: Prefix) =
|
|
|
559
559
|
}>;
|
|
560
560
|
}>;
|
|
561
561
|
}, {
|
|
562
|
-
readonly _tag: "PressedEscape";
|
|
563
|
-
} | {
|
|
564
562
|
readonly _tag: "CancelledPointer";
|
|
565
563
|
} | {
|
|
566
564
|
readonly _tag: "CompletedAnnounceReorder";
|
|
@@ -569,9 +567,6 @@ export declare const subscriptions: <Prefix extends string>(keyPrefix: Prefix) =
|
|
|
569
567
|
} | {
|
|
570
568
|
readonly _tag: "GotDragAndDropMessage";
|
|
571
569
|
readonly message: {
|
|
572
|
-
readonly _tag: "PressedArrowKey";
|
|
573
|
-
readonly direction: "Down" | "Left" | "NextContainer" | "PreviousContainer" | "Right" | "Up";
|
|
574
|
-
} | {
|
|
575
570
|
readonly _tag: "ActivatedKeyboardDrag";
|
|
576
571
|
readonly itemId: string;
|
|
577
572
|
readonly containerId: string;
|
|
@@ -598,6 +593,9 @@ export declare const subscriptions: <Prefix extends string>(keyPrefix: Prefix) =
|
|
|
598
593
|
readonly containerId: string;
|
|
599
594
|
readonly index: number;
|
|
600
595
|
}>;
|
|
596
|
+
} | {
|
|
597
|
+
readonly _tag: "PressedArrowKey";
|
|
598
|
+
readonly direction: "Down" | "Left" | "NextContainer" | "PreviousContainer" | "Right" | "Up";
|
|
601
599
|
} | {
|
|
602
600
|
readonly _tag: "PressedDraggable";
|
|
603
601
|
readonly itemId: string;
|
|
@@ -615,6 +613,8 @@ export declare const subscriptions: <Prefix extends string>(keyPrefix: Prefix) =
|
|
|
615
613
|
readonly clientX: number;
|
|
616
614
|
readonly clientY: number;
|
|
617
615
|
readonly maybeIndex: Option.Option<number>;
|
|
616
|
+
} | {
|
|
617
|
+
readonly _tag: "PressedEscape";
|
|
618
618
|
} | {
|
|
619
619
|
readonly _tag: "PressedGripKey";
|
|
620
620
|
readonly itemId: string;
|
|
@@ -710,8 +710,6 @@ export declare const subscriptions: <Prefix extends string>(keyPrefix: Prefix) =
|
|
|
710
710
|
}>;
|
|
711
711
|
}>;
|
|
712
712
|
}, {
|
|
713
|
-
readonly _tag: "PressedEscape";
|
|
714
|
-
} | {
|
|
715
713
|
readonly _tag: "CancelledPointer";
|
|
716
714
|
} | {
|
|
717
715
|
readonly _tag: "CompletedAnnounceReorder";
|
|
@@ -720,9 +718,6 @@ export declare const subscriptions: <Prefix extends string>(keyPrefix: Prefix) =
|
|
|
720
718
|
} | {
|
|
721
719
|
readonly _tag: "GotDragAndDropMessage";
|
|
722
720
|
readonly message: {
|
|
723
|
-
readonly _tag: "PressedArrowKey";
|
|
724
|
-
readonly direction: "Down" | "Left" | "NextContainer" | "PreviousContainer" | "Right" | "Up";
|
|
725
|
-
} | {
|
|
726
721
|
readonly _tag: "ActivatedKeyboardDrag";
|
|
727
722
|
readonly itemId: string;
|
|
728
723
|
readonly containerId: string;
|
|
@@ -749,6 +744,9 @@ export declare const subscriptions: <Prefix extends string>(keyPrefix: Prefix) =
|
|
|
749
744
|
readonly containerId: string;
|
|
750
745
|
readonly index: number;
|
|
751
746
|
}>;
|
|
747
|
+
} | {
|
|
748
|
+
readonly _tag: "PressedArrowKey";
|
|
749
|
+
readonly direction: "Down" | "Left" | "NextContainer" | "PreviousContainer" | "Right" | "Up";
|
|
752
750
|
} | {
|
|
753
751
|
readonly _tag: "PressedDraggable";
|
|
754
752
|
readonly itemId: string;
|
|
@@ -766,6 +764,8 @@ export declare const subscriptions: <Prefix extends string>(keyPrefix: Prefix) =
|
|
|
766
764
|
readonly clientX: number;
|
|
767
765
|
readonly clientY: number;
|
|
768
766
|
readonly maybeIndex: Option.Option<number>;
|
|
767
|
+
} | {
|
|
768
|
+
readonly _tag: "PressedEscape";
|
|
769
769
|
} | {
|
|
770
770
|
readonly _tag: "PressedGripKey";
|
|
771
771
|
readonly itemId: string;
|
|
@@ -857,8 +857,6 @@ export declare const subscriptions: <Prefix extends string>(keyPrefix: Prefix) =
|
|
|
857
857
|
}>;
|
|
858
858
|
}>;
|
|
859
859
|
}, {
|
|
860
|
-
readonly _tag: "PressedEscape";
|
|
861
|
-
} | {
|
|
862
860
|
readonly _tag: "CancelledPointer";
|
|
863
861
|
} | {
|
|
864
862
|
readonly _tag: "CompletedAnnounceReorder";
|
|
@@ -867,9 +865,6 @@ export declare const subscriptions: <Prefix extends string>(keyPrefix: Prefix) =
|
|
|
867
865
|
} | {
|
|
868
866
|
readonly _tag: "GotDragAndDropMessage";
|
|
869
867
|
readonly message: {
|
|
870
|
-
readonly _tag: "PressedArrowKey";
|
|
871
|
-
readonly direction: "Down" | "Left" | "NextContainer" | "PreviousContainer" | "Right" | "Up";
|
|
872
|
-
} | {
|
|
873
868
|
readonly _tag: "ActivatedKeyboardDrag";
|
|
874
869
|
readonly itemId: string;
|
|
875
870
|
readonly containerId: string;
|
|
@@ -896,6 +891,9 @@ export declare const subscriptions: <Prefix extends string>(keyPrefix: Prefix) =
|
|
|
896
891
|
readonly containerId: string;
|
|
897
892
|
readonly index: number;
|
|
898
893
|
}>;
|
|
894
|
+
} | {
|
|
895
|
+
readonly _tag: "PressedArrowKey";
|
|
896
|
+
readonly direction: "Down" | "Left" | "NextContainer" | "PreviousContainer" | "Right" | "Up";
|
|
899
897
|
} | {
|
|
900
898
|
readonly _tag: "PressedDraggable";
|
|
901
899
|
readonly itemId: string;
|
|
@@ -913,6 +911,8 @@ export declare const subscriptions: <Prefix extends string>(keyPrefix: Prefix) =
|
|
|
913
911
|
readonly clientX: number;
|
|
914
912
|
readonly clientY: number;
|
|
915
913
|
readonly maybeIndex: Option.Option<number>;
|
|
914
|
+
} | {
|
|
915
|
+
readonly _tag: "PressedEscape";
|
|
916
916
|
} | {
|
|
917
917
|
readonly _tag: "PressedGripKey";
|
|
918
918
|
readonly itemId: string;
|
|
@@ -1055,8 +1055,6 @@ export declare const view: import("foldkit/submodel").View<{
|
|
|
1055
1055
|
}>;
|
|
1056
1056
|
}>;
|
|
1057
1057
|
}, {
|
|
1058
|
-
readonly _tag: "PressedEscape";
|
|
1059
|
-
} | {
|
|
1060
1058
|
readonly _tag: "CancelledPointer";
|
|
1061
1059
|
} | {
|
|
1062
1060
|
readonly _tag: "CompletedAnnounceReorder";
|
|
@@ -1065,9 +1063,6 @@ export declare const view: import("foldkit/submodel").View<{
|
|
|
1065
1063
|
} | {
|
|
1066
1064
|
readonly _tag: "GotDragAndDropMessage";
|
|
1067
1065
|
readonly message: {
|
|
1068
|
-
readonly _tag: "PressedArrowKey";
|
|
1069
|
-
readonly direction: "Down" | "Left" | "NextContainer" | "PreviousContainer" | "Right" | "Up";
|
|
1070
|
-
} | {
|
|
1071
1066
|
readonly _tag: "ActivatedKeyboardDrag";
|
|
1072
1067
|
readonly itemId: string;
|
|
1073
1068
|
readonly containerId: string;
|
|
@@ -1094,6 +1089,9 @@ export declare const view: import("foldkit/submodel").View<{
|
|
|
1094
1089
|
readonly containerId: string;
|
|
1095
1090
|
readonly index: number;
|
|
1096
1091
|
}>;
|
|
1092
|
+
} | {
|
|
1093
|
+
readonly _tag: "PressedArrowKey";
|
|
1094
|
+
readonly direction: "Down" | "Left" | "NextContainer" | "PreviousContainer" | "Right" | "Up";
|
|
1097
1095
|
} | {
|
|
1098
1096
|
readonly _tag: "PressedDraggable";
|
|
1099
1097
|
readonly itemId: string;
|
|
@@ -1111,6 +1109,8 @@ export declare const view: import("foldkit/submodel").View<{
|
|
|
1111
1109
|
readonly clientX: number;
|
|
1112
1110
|
readonly clientY: number;
|
|
1113
1111
|
readonly maybeIndex: Option.Option<number>;
|
|
1112
|
+
} | {
|
|
1113
|
+
readonly _tag: "PressedEscape";
|
|
1114
1114
|
} | {
|
|
1115
1115
|
readonly _tag: "PressedGripKey";
|
|
1116
1116
|
readonly itemId: string;
|
|
@@ -326,6 +326,8 @@ export declare const update: (model: Model, message: Message) => Readonly<{
|
|
|
326
326
|
readonly pendingDismissVersion: number;
|
|
327
327
|
readonly isHovered: boolean;
|
|
328
328
|
readonly swipeState: {
|
|
329
|
+
readonly _tag: "Idle";
|
|
330
|
+
} | {
|
|
329
331
|
readonly _tag: "Dismissing";
|
|
330
332
|
readonly offsetX: number;
|
|
331
333
|
readonly direction: "Left" | "Right";
|
|
@@ -334,8 +336,6 @@ export declare const update: (model: Model, message: Message) => Readonly<{
|
|
|
334
336
|
readonly pointerId: number;
|
|
335
337
|
readonly startX: number;
|
|
336
338
|
readonly currentX: number;
|
|
337
|
-
} | {
|
|
338
|
-
readonly _tag: "Idle";
|
|
339
339
|
} | {
|
|
340
340
|
readonly _tag: "Settling";
|
|
341
341
|
readonly offsetX: number;
|
|
@@ -371,15 +371,15 @@ export declare const update: (model: Model, message: Message) => Readonly<{
|
|
|
371
371
|
readonly _tag: "GotAnimationMessage";
|
|
372
372
|
readonly entryId: string;
|
|
373
373
|
readonly message: {
|
|
374
|
+
readonly _tag: "Hid";
|
|
375
|
+
} | {
|
|
376
|
+
readonly _tag: "Showed";
|
|
377
|
+
} | {
|
|
374
378
|
readonly _tag: "CompletedWaitForPaint";
|
|
375
379
|
readonly generation: number;
|
|
376
380
|
} | {
|
|
377
381
|
readonly _tag: "EndedAnimation";
|
|
378
382
|
readonly generation: number;
|
|
379
|
-
} | {
|
|
380
|
-
readonly _tag: "Hid";
|
|
381
|
-
} | {
|
|
382
|
-
readonly _tag: "Showed";
|
|
383
383
|
};
|
|
384
384
|
} | {
|
|
385
385
|
readonly _tag: "HoveredEntry";
|
|
@@ -557,6 +557,8 @@ export declare const view: import("foldkit/submodel").View<{
|
|
|
557
557
|
readonly pendingDismissVersion: number;
|
|
558
558
|
readonly isHovered: boolean;
|
|
559
559
|
readonly swipeState: {
|
|
560
|
+
readonly _tag: "Idle";
|
|
561
|
+
} | {
|
|
560
562
|
readonly _tag: "Dismissing";
|
|
561
563
|
readonly offsetX: number;
|
|
562
564
|
readonly direction: "Left" | "Right";
|
|
@@ -565,8 +567,6 @@ export declare const view: import("foldkit/submodel").View<{
|
|
|
565
567
|
readonly pointerId: number;
|
|
566
568
|
readonly startX: number;
|
|
567
569
|
readonly currentX: number;
|
|
568
|
-
} | {
|
|
569
|
-
readonly _tag: "Idle";
|
|
570
570
|
} | {
|
|
571
571
|
readonly _tag: "Settling";
|
|
572
572
|
readonly offsetX: number;
|
|
@@ -602,15 +602,15 @@ export declare const view: import("foldkit/submodel").View<{
|
|
|
602
602
|
readonly _tag: "GotAnimationMessage";
|
|
603
603
|
readonly entryId: string;
|
|
604
604
|
readonly message: {
|
|
605
|
+
readonly _tag: "Hid";
|
|
606
|
+
} | {
|
|
607
|
+
readonly _tag: "Showed";
|
|
608
|
+
} | {
|
|
605
609
|
readonly _tag: "CompletedWaitForPaint";
|
|
606
610
|
readonly generation: number;
|
|
607
611
|
} | {
|
|
608
612
|
readonly _tag: "EndedAnimation";
|
|
609
613
|
readonly generation: number;
|
|
610
|
-
} | {
|
|
611
|
-
readonly _tag: "Hid";
|
|
612
|
-
} | {
|
|
613
|
-
readonly _tag: "Showed";
|
|
614
614
|
};
|
|
615
615
|
} | {
|
|
616
616
|
readonly _tag: "HoveredEntry";
|
|
@@ -12,7 +12,9 @@ export type ViewConfig<Message> = Readonly<{
|
|
|
12
12
|
glyph?: GlyphName;
|
|
13
13
|
/** A chip in the title bar: its state. */
|
|
14
14
|
status?: ReadonlyArray<Html | string>;
|
|
15
|
-
/** IconButtons in the title bar, before the close.
|
|
15
|
+
/** IconButtons in the title bar, before the close. A child Submodel, whose view is one
|
|
16
|
+
* `Html`, hands back several in a `span.window-tools`: it is no box of its own
|
|
17
|
+
* (`display: contents`), so its buttons sit in the bar as the others do. */
|
|
16
18
|
tools?: ReadonlyArray<Html>;
|
|
17
19
|
/** The close glyph's Message; the glyph prints when it is given. */
|
|
18
20
|
onClose?: Message;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"window.d.ts","sourceRoot":"","sources":["../../../src/window/window.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAOpD,wDAAwD;AACxD,MAAM,MAAM,UAAU,CAAC,OAAO,IAAI,QAAQ,CAAC;IACzC,oFAAoF;IACpF,EAAE,EAAE,MAAM,CAAC;IACX,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,IAAI,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC;IAC3B,+DAA+D;IAC/D,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,aAAa,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;IACtC
|
|
1
|
+
{"version":3,"file":"window.d.ts","sourceRoot":"","sources":["../../../src/window/window.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,IAAI,EAAE,WAAW,EAAE,MAAM,cAAc,CAAC;AAGtD,OAAO,KAAK,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAOpD,wDAAwD;AACxD,MAAM,MAAM,UAAU,CAAC,OAAO,IAAI,QAAQ,CAAC;IACzC,oFAAoF;IACpF,EAAE,EAAE,MAAM,CAAC;IACX,4CAA4C;IAC5C,KAAK,EAAE,MAAM,CAAC;IACd,+DAA+D;IAC/D,IAAI,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC;IAC3B,+DAA+D;IAC/D,KAAK,CAAC,EAAE,SAAS,CAAC;IAClB,0CAA0C;IAC1C,MAAM,CAAC,EAAE,aAAa,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;IACtC;;iFAE6E;IAC7E,KAAK,CAAC,EAAE,aAAa,CAAC,IAAI,CAAC,CAAC;IAC5B,oEAAoE;IACpE,OAAO,CAAC,EAAE,OAAO,CAAC;IAClB,IAAI,EAAE,aAAa,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;IACnC,iEAAiE;IACjE,IAAI,CAAC,EAAE,aAAa,CAAC,IAAI,GAAG,MAAM,CAAC,CAAC;IACpC,8DAA8D;IAC9D,OAAO,CAAC,EAAE,OAAO,CAAC;CACnB,CAAC,CAAC;AAEH;;6DAE6D;AAC7D,eAAO,MAAM,IAAI,GAAI,OAAO,UAAU,UAAU,CAAC,OAAO,CAAC,KAAK,WAAW,CAAC,OAAO,CAAC,KAAG,IAsCpF,CAAC"}
|
package/package.json
CHANGED
package/src/field/field.ts
CHANGED
|
@@ -48,6 +48,9 @@ export type ViewConfig<Message> = Readonly<{
|
|
|
48
48
|
isBoxed?: boolean;
|
|
49
49
|
/** Code and identifiers: the system monospace. */
|
|
50
50
|
isMono?: boolean;
|
|
51
|
+
/** The label is for screen readers only (`.sr-only`): a field whose place says what it is,
|
|
52
|
+
* such as a browser's address bar. It still names the input. */
|
|
53
|
+
isLabelHidden?: boolean;
|
|
51
54
|
/** The input's type (`"date"` for the browser's date control, always boxed); text when not
|
|
52
55
|
* given. */
|
|
53
56
|
type?: string;
|
|
@@ -71,6 +74,7 @@ export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Messag
|
|
|
71
74
|
isRequired = false,
|
|
72
75
|
isBoxed = false,
|
|
73
76
|
isMono = false,
|
|
77
|
+
isLabelHidden = false,
|
|
74
78
|
type,
|
|
75
79
|
inputAttributes = [],
|
|
76
80
|
} = config;
|
|
@@ -87,7 +91,7 @@ export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Messag
|
|
|
87
91
|
h.div(
|
|
88
92
|
[h.Class(`field${isBoxed || type === "date" ? " boxed" : ""}${isMono ? " mono" : ""}`)],
|
|
89
93
|
[
|
|
90
|
-
h.label(groups.label, [label]),
|
|
94
|
+
h.label([...groups.label, ...(isLabelHidden ? [h.Class("sr-only")] : [])], [label]),
|
|
91
95
|
// NOTE: @foldkit/ui gap: Input has no required option, so the wrapper says it.
|
|
92
96
|
h.input([
|
|
93
97
|
...inputAttributes,
|
package/src/lineDiff/index.ts
CHANGED
package/src/lineDiff/lineDiff.ts
CHANGED
|
@@ -1,15 +1,9 @@
|
|
|
1
|
-
import { Array, Option
|
|
1
|
+
import { Array, Option } from "effect";
|
|
2
2
|
import type { Html, HtmlBuilder } from "foldkit/html";
|
|
3
3
|
|
|
4
4
|
import { LOCALE } from "../locale/locale.ts";
|
|
5
|
-
import {
|
|
6
|
-
|
|
7
|
-
type LineKind,
|
|
8
|
-
diffRowsOf,
|
|
9
|
-
diffRowsOfPatch,
|
|
10
|
-
foldUnchanged,
|
|
11
|
-
hasChanges,
|
|
12
|
-
} from "./diff.ts";
|
|
5
|
+
import { DiffRow, type LineKind, diffRowsOf, foldUnchanged, hasChanges } from "./diff.ts";
|
|
6
|
+
import { SAME_NOTE, readPatch } from "./patch.ts";
|
|
13
7
|
|
|
14
8
|
// VIEW
|
|
15
9
|
|
|
@@ -29,7 +23,8 @@ export type ViewConfig = Readonly<{
|
|
|
29
23
|
|
|
30
24
|
/** Configuration for rendering a unified diff with `patchView`. */
|
|
31
25
|
export type PatchViewConfig = Readonly<{
|
|
32
|
-
/** One file's unified diff, as `diff -u` or `git diff -- <path>` writes it
|
|
26
|
+
/** One file's unified diff, as `diff -u` or `git diff -- <path>` writes it, its git header
|
|
27
|
+
* included. */
|
|
33
28
|
patch: string;
|
|
34
29
|
/** Names the two sides for screen readers: `"Version 3 against version 4"`. */
|
|
35
30
|
label: string;
|
|
@@ -86,8 +81,6 @@ const rowView = <Message>(row: DiffRow, h: HtmlBuilder<Message>): Html =>
|
|
|
86
81
|
const noteView = <Message>(note: string, h: HtmlBuilder<Message>): Html =>
|
|
87
82
|
h.p([h.Class("note-line")], [note]);
|
|
88
83
|
|
|
89
|
-
const SAME_NOTE = "No difference: both sides hold the same text.";
|
|
90
|
-
|
|
91
84
|
const tableView = <Message>(
|
|
92
85
|
rows: ReadonlyArray<DiffRow>,
|
|
93
86
|
label: string,
|
|
@@ -123,18 +116,30 @@ export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html
|
|
|
123
116
|
/** A unified diff the caller already has (from a server, or `git diff -- <path>`) as `view`'s
|
|
124
117
|
* table: its hunks' lines, numbered from their headers, and the unchanged lines between hunks
|
|
125
118
|
* folded into a counted row. Lines past the last hunk are not in the patch, so nothing counts
|
|
126
|
-
* them.
|
|
127
|
-
*
|
|
128
|
-
*
|
|
119
|
+
* them. What git's header says beyond the lines prints as a note line above the table, or in
|
|
120
|
+
* its place: a rename, a mode change, an empty or binary file (`describePatch` has the
|
|
121
|
+
* sentences). A file whose type changed (a file that became a symlink) is two sections, each
|
|
122
|
+
* its own table under one note, named for its side (`"<label>, as it was"`). An empty patch
|
|
123
|
+
* says there is no difference, as `view` does; text that is not a diff, or holds several
|
|
124
|
+
* files, says it cannot be shown as lines. */
|
|
129
125
|
export const patchView = <Message>(config: PatchViewConfig, h: HtmlBuilder<Message>): Html => {
|
|
130
126
|
const { patch, label } = config;
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
127
|
+
const { notes, tables } = readPatch(patch);
|
|
128
|
+
const parts = [
|
|
129
|
+
...(Array.isReadonlyArrayNonEmpty(notes) ? [noteView(Array.join(notes, " "), h)] : []),
|
|
130
|
+
...Array.map(tables, ({ rows, maybeSide }) =>
|
|
131
|
+
tableView(
|
|
132
|
+
rows,
|
|
133
|
+
Option.match(maybeSide, { onNone: () => label, onSome: (side) => `${label}, ${side}` }),
|
|
134
|
+
h,
|
|
135
|
+
),
|
|
136
|
+
),
|
|
137
|
+
];
|
|
138
|
+
|
|
139
|
+
// a lone table or note is drawn bare, so a flush window's body still meets its table
|
|
140
|
+
return Array.match(parts, {
|
|
141
|
+
onEmpty: () => noteView(SAME_NOTE, h),
|
|
142
|
+
onNonEmpty: ([part, ...others]) =>
|
|
143
|
+
Array.isArrayEmpty(others) ? part : h.div([h.Class("stack gap-s")], parts),
|
|
144
|
+
});
|
|
140
145
|
};
|
|
@@ -0,0 +1,218 @@
|
|
|
1
|
+
import { Array, Option, String, pipe } from "effect";
|
|
2
|
+
|
|
3
|
+
import { type DiffRow, diffRowsOfPatch } from "./diff.ts";
|
|
4
|
+
|
|
5
|
+
// QUOTED PATHS
|
|
6
|
+
|
|
7
|
+
const ESCAPED_BYTES: Readonly<Record<string, number>> = {
|
|
8
|
+
a: 7,
|
|
9
|
+
b: 8,
|
|
10
|
+
t: 9,
|
|
11
|
+
n: 10,
|
|
12
|
+
v: 11,
|
|
13
|
+
f: 12,
|
|
14
|
+
r: 13,
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
// an octal escape is one byte of the name's UTF-8, so the bytes are gathered before decoding
|
|
18
|
+
const QUOTED_PART = /\\([0-7]{3})|\\(.)|[^\\]+/gu;
|
|
19
|
+
|
|
20
|
+
const bytesOf = (text: string): ReadonlyArray<number> => [...new TextEncoder().encode(text)];
|
|
21
|
+
|
|
22
|
+
const bytesOfPart = ([part, octal, escaped]: RegExpExecArray): ReadonlyArray<number> => {
|
|
23
|
+
if (octal !== undefined) {
|
|
24
|
+
return [Number.parseInt(octal, 8)];
|
|
25
|
+
} else if (escaped !== undefined) {
|
|
26
|
+
return Option.match(Option.fromUndefinedOr(ESCAPED_BYTES[escaped]), {
|
|
27
|
+
onNone: () => bytesOf(escaped),
|
|
28
|
+
onSome: (byte) => [byte],
|
|
29
|
+
});
|
|
30
|
+
} else {
|
|
31
|
+
return bytesOf(part);
|
|
32
|
+
}
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
/** The name a path in git's header stands for: git C-quotes a name with unusual bytes
|
|
36
|
+
* (`"caf\303\251.txt"`), and leaves any other as it is. */
|
|
37
|
+
export const unquoted = (path: string): string =>
|
|
38
|
+
path.length >= 2 && path.startsWith('"') && path.endsWith('"')
|
|
39
|
+
? new TextDecoder().decode(
|
|
40
|
+
Uint8Array.from(
|
|
41
|
+
Array.flatMap(Array.fromIterable(path.slice(1, -1).matchAll(QUOTED_PART)), bytesOfPart),
|
|
42
|
+
),
|
|
43
|
+
)
|
|
44
|
+
: path;
|
|
45
|
+
|
|
46
|
+
// HEADER
|
|
47
|
+
|
|
48
|
+
const GIT_SECTION = "diff --git ";
|
|
49
|
+
|
|
50
|
+
// the header is what comes before the first hunk, so a changed line that reads like a header
|
|
51
|
+
// line ("+rename from x") is never taken for one
|
|
52
|
+
const headOf = (section: string): ReadonlyArray<string> =>
|
|
53
|
+
Array.takeWhile(
|
|
54
|
+
section.split("\n"),
|
|
55
|
+
(line) => !line.startsWith("@@") && !line.startsWith("--- "),
|
|
56
|
+
);
|
|
57
|
+
|
|
58
|
+
const valueAfter = (head: ReadonlyArray<string>, prefix: string): Option.Option<string> =>
|
|
59
|
+
pipe(
|
|
60
|
+
Array.findFirst(head, String.startsWith(prefix)),
|
|
61
|
+
Option.map((line) => line.slice(prefix.length)),
|
|
62
|
+
);
|
|
63
|
+
|
|
64
|
+
const hasLine = (head: ReadonlyArray<string>, prefix: string): boolean =>
|
|
65
|
+
Option.isSome(valueAfter(head, prefix));
|
|
66
|
+
|
|
67
|
+
const hasHunk = (section: string): boolean =>
|
|
68
|
+
Array.some(section.split("\n"), String.startsWith("@@ "));
|
|
69
|
+
|
|
70
|
+
// git writes one section per file, after whatever precedes the first (a commit's message, a
|
|
71
|
+
// mail's head); a patch without a git header is one section
|
|
72
|
+
const sectionsOf = (patch: string): ReadonlyArray<string> => {
|
|
73
|
+
const [first = "", ...others] = patch.split(/(?=^diff --git )/mu);
|
|
74
|
+
|
|
75
|
+
return Array.isArrayNonEmpty(others) && !first.startsWith(GIT_SECTION)
|
|
76
|
+
? others
|
|
77
|
+
: [first, ...others];
|
|
78
|
+
};
|
|
79
|
+
|
|
80
|
+
// READING
|
|
81
|
+
|
|
82
|
+
/** What a diff of two same texts says. */
|
|
83
|
+
export const SAME_NOTE = "No difference: both sides hold the same text.";
|
|
84
|
+
const UNREADABLE_NOTE = "This diff cannot be shown as lines.";
|
|
85
|
+
const BINARY_NOTE = "A binary file: its change has no lines.";
|
|
86
|
+
|
|
87
|
+
/** A table of the patch's lines; `maybeSide` says which side of a type change it is. */
|
|
88
|
+
type Table = Readonly<{ rows: ReadonlyArray<DiffRow>; maybeSide: Option.Option<string> }>;
|
|
89
|
+
|
|
90
|
+
/** What `patchView` draws: its notes as a line, then its tables. */
|
|
91
|
+
export type PatchReading = Readonly<{
|
|
92
|
+
maybeRenamedFrom: Option.Option<string>;
|
|
93
|
+
notes: ReadonlyArray<string>;
|
|
94
|
+
tables: ReadonlyArray<Table>;
|
|
95
|
+
}>;
|
|
96
|
+
|
|
97
|
+
const unreadable: PatchReading = {
|
|
98
|
+
maybeRenamedFrom: Option.none(),
|
|
99
|
+
notes: [UNREADABLE_NOTE],
|
|
100
|
+
tables: [],
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
const lineNote = (head: ReadonlyArray<string>, hasRows: boolean): Option.Option<string> => {
|
|
104
|
+
if (hasLine(head, "Binary files ") || hasLine(head, "GIT binary patch")) {
|
|
105
|
+
return Option.some(BINARY_NOTE);
|
|
106
|
+
} else if (hasRows) {
|
|
107
|
+
return Option.none();
|
|
108
|
+
} else if (hasLine(head, "new file mode ")) {
|
|
109
|
+
return Option.some("New empty file.");
|
|
110
|
+
} else if (hasLine(head, "deleted file mode ")) {
|
|
111
|
+
return Option.some("Deleted empty file.");
|
|
112
|
+
} else {
|
|
113
|
+
return Option.none();
|
|
114
|
+
}
|
|
115
|
+
};
|
|
116
|
+
|
|
117
|
+
const readSection = (section: string): PatchReading => {
|
|
118
|
+
const head = headOf(section);
|
|
119
|
+
const maybeRows = diffRowsOfPatch(section);
|
|
120
|
+
const maybeRenamedFrom = Option.map(valueAfter(head, "rename from "), unquoted);
|
|
121
|
+
const notes = [
|
|
122
|
+
...Option.toArray(Option.map(maybeRenamedFrom, (from) => `Renamed from ${from}.`)),
|
|
123
|
+
...Option.toArray(
|
|
124
|
+
Option.map(
|
|
125
|
+
Option.all({ before: valueAfter(head, "old mode "), after: valueAfter(head, "new mode ") }),
|
|
126
|
+
({ before, after }) => `Mode ${before} → ${after}.`,
|
|
127
|
+
),
|
|
128
|
+
),
|
|
129
|
+
...Option.toArray(lineNote(head, Option.isSome(maybeRows))),
|
|
130
|
+
];
|
|
131
|
+
const isUnread = Option.isNone(maybeRows) && (hasHunk(section) || Array.isArrayEmpty(notes));
|
|
132
|
+
|
|
133
|
+
return isUnread
|
|
134
|
+
? unreadable
|
|
135
|
+
: {
|
|
136
|
+
maybeRenamedFrom,
|
|
137
|
+
notes,
|
|
138
|
+
tables: Option.toArray(
|
|
139
|
+
Option.map(maybeRows, (rows) => ({ rows, maybeSide: Option.none() })),
|
|
140
|
+
),
|
|
141
|
+
};
|
|
142
|
+
};
|
|
143
|
+
|
|
144
|
+
const KINDS: Readonly<Record<string, string>> = {
|
|
145
|
+
"100644": "a file",
|
|
146
|
+
"100755": "an executable file",
|
|
147
|
+
"120000": "a symlink",
|
|
148
|
+
"160000": "a submodule",
|
|
149
|
+
};
|
|
150
|
+
|
|
151
|
+
const kindOf = (mode: string): string => KINDS[mode] ?? `mode ${mode}`;
|
|
152
|
+
|
|
153
|
+
const sideTable = (section: string, side: string): ReadonlyArray<Table> =>
|
|
154
|
+
Option.toArray(
|
|
155
|
+
Option.map(diffRowsOfPatch(section), (rows) => ({ rows, maybeSide: Option.some(side) })),
|
|
156
|
+
);
|
|
157
|
+
|
|
158
|
+
// a file that became a symlink (or any other kind) is two sections for one path: the old
|
|
159
|
+
// kind deleted, then the new one added
|
|
160
|
+
const readTypeChange = (gone: string, added: string): Option.Option<PatchReading> => {
|
|
161
|
+
const [goneHead, addedHead] = [headOf(gone), headOf(added)];
|
|
162
|
+
|
|
163
|
+
return Option.flatMap(
|
|
164
|
+
Option.all({
|
|
165
|
+
before: valueAfter(goneHead, "deleted file mode "),
|
|
166
|
+
after: valueAfter(addedHead, "new file mode "),
|
|
167
|
+
}),
|
|
168
|
+
({ before, after }) =>
|
|
169
|
+
goneHead[0] === addedHead[0]
|
|
170
|
+
? Option.some({
|
|
171
|
+
maybeRenamedFrom: Option.none(),
|
|
172
|
+
notes: [`It was ${kindOf(before)} and is now ${kindOf(after)}.`],
|
|
173
|
+
tables: [...sideTable(gone, "as it was"), ...sideTable(added, "as it is")],
|
|
174
|
+
})
|
|
175
|
+
: Option.none(),
|
|
176
|
+
);
|
|
177
|
+
};
|
|
178
|
+
|
|
179
|
+
/** What `patchView` makes of a patch: the notes it prints and the tables it draws. */
|
|
180
|
+
export const readPatch = (patch: string): PatchReading => {
|
|
181
|
+
const text = patch.replaceAll("\r\n", "\n");
|
|
182
|
+
|
|
183
|
+
if (String.isEmpty(String.trim(text))) {
|
|
184
|
+
return { maybeRenamedFrom: Option.none(), notes: [SAME_NOTE], tables: [] };
|
|
185
|
+
} else {
|
|
186
|
+
const sections = sectionsOf(text);
|
|
187
|
+
const [first, second, ...rest] = sections;
|
|
188
|
+
|
|
189
|
+
if (first !== undefined && second === undefined) {
|
|
190
|
+
return readSection(first);
|
|
191
|
+
} else if (first !== undefined && second !== undefined && Array.isArrayEmpty(rest)) {
|
|
192
|
+
return Option.getOrElse(readTypeChange(first, second), () => unreadable);
|
|
193
|
+
} else {
|
|
194
|
+
return unreadable;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
// DESCRIPTION
|
|
200
|
+
|
|
201
|
+
/** What a patch says beyond its lines, as `patchView` prints it. */
|
|
202
|
+
export type PatchDescription = Readonly<{
|
|
203
|
+
/** The path a rename left, unquoted: for a heading such as `old.txt → new.txt`. */
|
|
204
|
+
maybeRenamedFrom: Option.Option<string>;
|
|
205
|
+
/** The sentences `patchView` prints above the patch's lines, or in their place: a rename,
|
|
206
|
+
* a mode change, a type change, an empty or binary file, no difference, or a patch it
|
|
207
|
+
* cannot show as lines. */
|
|
208
|
+
notes: ReadonlyArray<string>;
|
|
209
|
+
}>;
|
|
210
|
+
|
|
211
|
+
/** What `patchView` says about a patch from `git diff`, read from its header: the notes it
|
|
212
|
+
* prints (`["Renamed from old.txt."]`), for a caller that prints them itself, and the path a
|
|
213
|
+
* rename left. */
|
|
214
|
+
export const describePatch = (patch: string): PatchDescription => {
|
|
215
|
+
const { maybeRenamedFrom, notes } = readPatch(patch);
|
|
216
|
+
|
|
217
|
+
return { maybeRenamedFrom, notes };
|
|
218
|
+
};
|
package/src/pixel-paper.css
CHANGED
|
@@ -5151,6 +5151,12 @@ dialog.veil:focus {
|
|
|
5151
5151
|
color: inherit;
|
|
5152
5152
|
}
|
|
5153
5153
|
|
|
5154
|
+
/* a child Submodel's tools come back as one element; the group is no box of its own, so its
|
|
5155
|
+
buttons take the bar's gap as the others do */
|
|
5156
|
+
.window-tools {
|
|
5157
|
+
display: contents;
|
|
5158
|
+
}
|
|
5159
|
+
|
|
5154
5160
|
.window-body {
|
|
5155
5161
|
flex: 1;
|
|
5156
5162
|
min-height: 0;
|
package/src/window/window.ts
CHANGED
|
@@ -20,7 +20,9 @@ export type ViewConfig<Message> = Readonly<{
|
|
|
20
20
|
glyph?: GlyphName;
|
|
21
21
|
/** A chip in the title bar: its state. */
|
|
22
22
|
status?: ReadonlyArray<Html | string>;
|
|
23
|
-
/** IconButtons in the title bar, before the close.
|
|
23
|
+
/** IconButtons in the title bar, before the close. A child Submodel, whose view is one
|
|
24
|
+
* `Html`, hands back several in a `span.window-tools`: it is no box of its own
|
|
25
|
+
* (`display: contents`), so its buttons sit in the bar as the others do. */
|
|
24
26
|
tools?: ReadonlyArray<Html>;
|
|
25
27
|
/** The close glyph's Message; the glyph prints when it is given. */
|
|
26
28
|
onClose?: Message;
|