@gjermundgaraba-designs/pixel-paper 0.13.0 → 0.14.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 +29 -22
- package/dist/src/announcer/announcer.d.ts +34 -26
- package/dist/src/announcer/announcer.d.ts.map +1 -1
- package/dist/src/announcer/announcer.js +42 -32
- package/dist/src/announcer/index.d.ts +1 -1
- package/dist/src/announcer/index.d.ts.map +1 -1
- package/dist/src/announcer/index.js +1 -1
- package/dist/src/banner/banner.d.ts +1 -1
- package/dist/src/banner/banner.js +1 -1
- package/dist/src/barChart/barChart.js +1 -1
- package/dist/src/board/board.d.ts +1 -4
- package/dist/src/board/board.d.ts.map +1 -1
- package/dist/src/board/board.js +8 -71
- package/dist/src/button/button.d.ts +1 -1
- package/dist/src/button/button.js +1 -1
- package/dist/src/card/card.d.ts +35 -9
- package/dist/src/card/card.d.ts.map +1 -1
- package/dist/src/card/card.js +46 -8
- package/dist/src/card/index.d.ts +2 -2
- package/dist/src/card/index.d.ts.map +1 -1
- package/dist/src/card/index.js +1 -1
- package/dist/src/commandPalette/commandPalette.d.ts +4 -0
- package/dist/src/commandPalette/commandPalette.d.ts.map +1 -1
- package/dist/src/confirmDialog/confirmDialog.d.ts +18 -26
- package/dist/src/confirmDialog/confirmDialog.d.ts.map +1 -1
- package/dist/src/confirmDialog/confirmDialog.js +21 -19
- package/dist/src/confirmDialog/index.d.ts +1 -1
- package/dist/src/confirmDialog/index.d.ts.map +1 -1
- package/dist/src/confirmDialog/index.js +1 -1
- package/dist/src/contextMenu/contextMenu.d.ts +6 -6
- package/dist/src/dialog/dialog.d.ts +36 -7
- package/dist/src/dialog/dialog.d.ts.map +1 -1
- package/dist/src/dialog/dialog.js +32 -12
- package/dist/src/dialog/index.d.ts +1 -1
- package/dist/src/dialog/index.d.ts.map +1 -1
- package/dist/src/dialog/index.js +1 -1
- package/dist/src/drag/drag.d.ts +40 -0
- package/dist/src/drag/drag.d.ts.map +1 -0
- package/dist/src/drag/drag.js +134 -0
- package/dist/src/emptyState/emptyState.d.ts +6 -4
- package/dist/src/emptyState/emptyState.d.ts.map +1 -1
- package/dist/src/emptyState/emptyState.js +1 -1
- package/dist/src/errorSummary/errorSummary.d.ts +1 -1
- package/dist/src/errorSummary/errorSummary.js +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/keyHelp/keyHelp.d.ts +4 -0
- package/dist/src/keyHelp/keyHelp.d.ts.map +1 -1
- package/dist/src/lineChart/lineChart.js +1 -1
- package/dist/src/liveRegion/index.d.ts +3 -0
- package/dist/src/liveRegion/index.d.ts.map +1 -0
- package/dist/src/liveRegion/index.js +1 -0
- package/dist/src/liveRegion/liveRegion.d.ts +42 -0
- package/dist/src/liveRegion/liveRegion.d.ts.map +1 -0
- package/dist/src/liveRegion/liveRegion.js +35 -0
- package/dist/src/loading/loading.d.ts +8 -3
- package/dist/src/loading/loading.d.ts.map +1 -1
- package/dist/src/loading/loading.js +4 -3
- package/dist/src/menu/menu.d.ts +6 -6
- package/dist/src/meter/meter.d.ts +3 -0
- package/dist/src/meter/meter.d.ts.map +1 -1
- package/dist/src/meter/meter.js +6 -3
- package/dist/src/remote/index.d.ts +3 -0
- package/dist/src/remote/index.d.ts.map +1 -0
- package/dist/src/remote/index.js +1 -0
- package/dist/src/remote/remote.d.ts +29 -0
- package/dist/src/remote/remote.d.ts.map +1 -0
- package/dist/src/remote/remote.js +41 -0
- package/dist/src/skipLink/index.d.ts +1 -1
- package/dist/src/skipLink/index.d.ts.map +1 -1
- package/dist/src/skipLink/index.js +1 -1
- package/dist/src/skipLink/skipLink.d.ts +24 -8
- package/dist/src/skipLink/skipLink.d.ts.map +1 -1
- package/dist/src/skipLink/skipLink.js +27 -8
- package/dist/src/sortable/index.d.ts +3 -0
- package/dist/src/sortable/index.d.ts.map +1 -0
- package/dist/src/sortable/index.js +1 -0
- package/dist/src/sortable/sortable.d.ts +1144 -0
- package/dist/src/sortable/sortable.d.ts.map +1 -0
- package/dist/src/sortable/sortable.js +504 -0
- package/dist/src/toast/index.d.ts +1 -1
- package/dist/src/toast/index.d.ts.map +1 -1
- package/dist/src/toast/index.js +1 -1
- package/dist/src/toast/toast.d.ts +32 -20
- package/dist/src/toast/toast.d.ts.map +1 -1
- package/dist/src/toast/toast.js +8 -3
- package/dist/src/topBar/barHeight.d.ts +6 -0
- package/dist/src/topBar/barHeight.d.ts.map +1 -1
- package/dist/src/topBar/barHeight.js +4 -0
- package/dist/src/topBar/index.d.ts +1 -1
- package/dist/src/topBar/index.d.ts.map +1 -1
- package/dist/src/topBar/index.js +1 -1
- package/dist/src/topBar/topBar.d.ts +1 -2
- package/dist/src/topBar/topBar.d.ts.map +1 -1
- package/package.json +1 -1
- package/src/announcer/announcer.ts +48 -32
- package/src/announcer/index.ts +1 -1
- package/src/banner/banner.ts +1 -1
- package/src/barChart/barChart.ts +1 -1
- package/src/board/board.ts +18 -87
- package/src/button/button.ts +1 -1
- package/src/card/card.ts +120 -21
- package/src/card/index.ts +2 -2
- package/src/confirmDialog/confirmDialog.ts +25 -32
- package/src/confirmDialog/index.ts +2 -0
- package/src/dialog/dialog.ts +39 -13
- package/src/dialog/index.ts +3 -1
- package/src/drag/drag.ts +207 -0
- package/src/emptyState/emptyState.ts +7 -5
- package/src/errorSummary/errorSummary.ts +1 -1
- package/src/index.ts +3 -0
- package/src/lineChart/lineChart.ts +1 -1
- package/src/liveRegion/index.ts +2 -0
- package/src/liveRegion/liveRegion.ts +75 -0
- package/src/loading/loading.ts +11 -5
- package/src/meter/meter.ts +17 -3
- package/src/pixel-paper.css +182 -27
- package/src/remote/index.ts +2 -0
- package/src/remote/remote.ts +76 -0
- package/src/skipLink/index.ts +1 -1
- package/src/skipLink/skipLink.ts +38 -9
- package/src/sortable/index.ts +14 -0
- package/src/sortable/sortable.ts +857 -0
- package/src/toast/index.ts +1 -0
- package/src/toast/toast.ts +9 -3
- package/src/tokens.css +13 -11
- package/src/topBar/barHeight.ts +6 -0
- package/src/topBar/index.ts +1 -1
- package/src/topBar/topBar.ts +1 -2
|
@@ -7,10 +7,12 @@ import type { Html, HtmlBuilder } from "foldkit/html";
|
|
|
7
7
|
export type ViewConfig = Readonly<{
|
|
8
8
|
/** Pixel caps: what is missing (`"Nothing queued"`). */
|
|
9
9
|
title: string;
|
|
10
|
-
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
|
|
10
|
+
/** The title's element, which looks the same either way. A heading at this level where the
|
|
11
|
+
* empty state is the page's or a section's main content: `3` inside a Panel, whose title is
|
|
12
|
+
* an `h2`; `1` for a gate or a missing page. `"Strong"` where it is not (a chart with no
|
|
13
|
+
* data, a picture of the state). Required, because the two look alike and only the outline
|
|
14
|
+
* tells them apart. */
|
|
15
|
+
headingLevel: 1 | 2 | 3 | 4 | 5 | 6 | "Strong";
|
|
14
16
|
/** One sentence: how it gets filled. */
|
|
15
17
|
body?: ReadonlyArray<Html | string>;
|
|
16
18
|
/** What fills it: a button under the sentence. */
|
|
@@ -24,7 +26,7 @@ export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html
|
|
|
24
26
|
return h.div(
|
|
25
27
|
[h.Class("empty")],
|
|
26
28
|
[
|
|
27
|
-
headingLevel ===
|
|
29
|
+
headingLevel === "Strong"
|
|
28
30
|
? h.strong([h.Class("empty-title")], [title])
|
|
29
31
|
: h[`h${headingLevel}`]([h.Class("empty-title")], [title]),
|
|
30
32
|
...Array.match(body, { onEmpty: () => [], onNonEmpty: (sentence) => [h.p([], sentence)] }),
|
|
@@ -39,7 +39,7 @@ const headline = (count: number): string =>
|
|
|
39
39
|
* busy) is a failed submit like any other: render the summary from it and focus it.
|
|
40
40
|
*
|
|
41
41
|
* Each link is a fragment link to its field; a Foldkit application's fragment handler moves
|
|
42
|
-
* focus there (
|
|
42
|
+
* focus there (`SkipLink.ScrollToFragment`).
|
|
43
43
|
*/
|
|
44
44
|
export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html => {
|
|
45
45
|
const { id, errors } = config;
|
package/src/index.ts
CHANGED
|
@@ -46,6 +46,7 @@ export * as LineChart from "./lineChart/index.ts";
|
|
|
46
46
|
export * as LineDiff from "./lineDiff/index.ts";
|
|
47
47
|
export * as LinkButton from "./linkButton/index.ts";
|
|
48
48
|
export * as LinkState from "./linkState/index.ts";
|
|
49
|
+
export * as LiveRegion from "./liveRegion/index.ts";
|
|
49
50
|
export * as Loading from "./loading/index.ts";
|
|
50
51
|
export * as Mark from "./mark/index.ts";
|
|
51
52
|
export * as Media from "./media/index.ts";
|
|
@@ -64,6 +65,7 @@ export * as Prose from "./prose/index.ts";
|
|
|
64
65
|
export * as QueryChip from "./queryChip/index.ts";
|
|
65
66
|
export * as RadioChoices from "./radioChoices/index.ts";
|
|
66
67
|
export * as Rail from "./rail/index.ts";
|
|
68
|
+
export * as Remote from "./remote/index.ts";
|
|
67
69
|
export * as Rows from "./rows/index.ts";
|
|
68
70
|
export * as Scheme from "./scheme/index.ts";
|
|
69
71
|
export * as Secret from "./secret/index.ts";
|
|
@@ -72,6 +74,7 @@ export * as SelectAll from "./selectAll/index.ts";
|
|
|
72
74
|
export * as SelectField from "./selectField/index.ts";
|
|
73
75
|
export * as Sheet from "./sheet/index.ts";
|
|
74
76
|
export * as SkipLink from "./skipLink/index.ts";
|
|
77
|
+
export * as Sortable from "./sortable/index.ts";
|
|
75
78
|
export * as Stack from "./stack/index.ts";
|
|
76
79
|
export * as Stage from "./stage/index.ts";
|
|
77
80
|
export * as Stat from "./stat/index.ts";
|
|
@@ -221,6 +221,6 @@ const chartView = <Message>(
|
|
|
221
221
|
*/
|
|
222
222
|
export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html =>
|
|
223
223
|
Array.match(config.labels, {
|
|
224
|
-
onEmpty: () => EmptyState.view({ title: "nothing to chart" }, h),
|
|
224
|
+
onEmpty: () => EmptyState.view({ title: "nothing to chart", headingLevel: "Strong" }, h),
|
|
225
225
|
onNonEmpty: (labels) => chartView(config, labels, h),
|
|
226
226
|
});
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Array, Option, pipe } from "effect";
|
|
2
|
+
import type { Html, HtmlBuilder } from "foldkit/html";
|
|
3
|
+
|
|
4
|
+
import * as Announcer from "../announcer/announcer.ts";
|
|
5
|
+
|
|
6
|
+
// PLACE
|
|
7
|
+
|
|
8
|
+
/** A dialog that can host the page's live parts: its name in the application, and whether it
|
|
9
|
+
* is open now. */
|
|
10
|
+
export type DialogEntry<Host extends string> = readonly [host: Host, isOpen: boolean];
|
|
11
|
+
|
|
12
|
+
/** Where the page's live parts go on this render. */
|
|
13
|
+
export type Placement<Host extends string> = Readonly<{
|
|
14
|
+
/** The `toLiveRegionView` to give the dialog `host`: the parts while it is the topmost open
|
|
15
|
+
* dialog, `undefined` while it is not. */
|
|
16
|
+
toDialogView: (host: Host) => (() => Html) | undefined;
|
|
17
|
+
/** The parts for the end of the body while no dialog is open, nothing while one is. */
|
|
18
|
+
toPageView: () => ReadonlyArray<Html>;
|
|
19
|
+
}>;
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Places the page's live parts by the hosting rule: an open Dialog makes the page outside it
|
|
23
|
+
* inert and hidden from screen readers, so they render inside the topmost open dialog, and at
|
|
24
|
+
* the end of the body while none is open, exactly once either way. Neither renders them
|
|
25
|
+
* until called, so a Submodel among the parts (the toast stack) is built once. `dialogs` lists every dialog
|
|
26
|
+
* that can host them from the bottom of the stack to the top: one that opens from another
|
|
27
|
+
* (a confirm from a form) comes after it, so of the open ones the last is the topmost. Call it
|
|
28
|
+
* on every render, from the application's Model, so the parts move as dialogs open and close.
|
|
29
|
+
*
|
|
30
|
+
* ```ts
|
|
31
|
+
* const placement = LiveRegion.place([
|
|
32
|
+
* ["Rename", Dialog.isOpen(model.rename)],
|
|
33
|
+
* ["Delete", ConfirmDialog.isOpen(model.deleteConfirm)],
|
|
34
|
+
* ], () => LiveRegion.view({ toasts: toastsView(model, h) }, h));
|
|
35
|
+
* // the rename dialog's view inputs: toLiveRegionView: placement.toDialogView("Rename")
|
|
36
|
+
* // the body: [...content, ...placement.toPageView()]
|
|
37
|
+
* ```
|
|
38
|
+
*/
|
|
39
|
+
export const place = <Host extends string>(
|
|
40
|
+
dialogs: ReadonlyArray<DialogEntry<Host>>,
|
|
41
|
+
toView: () => Html,
|
|
42
|
+
): Placement<Host> => {
|
|
43
|
+
const maybeTopmost = pipe(
|
|
44
|
+
Array.findLast(dialogs, ([, isOpen]) => isOpen),
|
|
45
|
+
Option.map(([host]) => host),
|
|
46
|
+
);
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
toDialogView: (host) =>
|
|
50
|
+
Option.exists(maybeTopmost, (topmost) => topmost === host) ? toView : undefined,
|
|
51
|
+
toPageView: () => (Option.isNone(maybeTopmost) ? [toView()] : []),
|
|
52
|
+
};
|
|
53
|
+
};
|
|
54
|
+
|
|
55
|
+
// VIEW
|
|
56
|
+
|
|
57
|
+
/** Configuration for rendering the page's live parts with `view`. */
|
|
58
|
+
export type ViewConfig = Readonly<{
|
|
59
|
+
/** The page's Toast stack (its `h.submodel`), when it has one. */
|
|
60
|
+
toasts?: Html;
|
|
61
|
+
/** The Banner in force, when one is: it stays in reach while a dialog is open. */
|
|
62
|
+
banner?: Html;
|
|
63
|
+
}>;
|
|
64
|
+
|
|
65
|
+
/** The page's live parts together, for `place`: the Announcer's region, then the toast stack
|
|
66
|
+
* and the banner the application gives. */
|
|
67
|
+
export const view = <Message>(config: ViewConfig, h: HtmlBuilder<Message>): Html =>
|
|
68
|
+
h.div(
|
|
69
|
+
[],
|
|
70
|
+
[
|
|
71
|
+
Announcer.view(h),
|
|
72
|
+
...(config.toasts === undefined ? [] : [config.toasts]),
|
|
73
|
+
...(config.banner === undefined ? [] : [config.banner]),
|
|
74
|
+
],
|
|
75
|
+
);
|
package/src/loading/loading.ts
CHANGED
|
@@ -1,25 +1,31 @@
|
|
|
1
1
|
import type { Html, HtmlBuilder } from "foldkit/html";
|
|
2
2
|
|
|
3
|
+
import type { Attributes } from "../attributes/attributes.ts";
|
|
4
|
+
|
|
3
5
|
// VIEW
|
|
4
6
|
|
|
5
7
|
/** Configuration for rendering a loading line with `view`. */
|
|
6
|
-
export type ViewConfig = Readonly<{
|
|
8
|
+
export type ViewConfig<Message> = Readonly<{
|
|
7
9
|
/** Whether the content is on its way. Done, the line empties and leaves the flow, so it
|
|
8
10
|
* takes no room and no gap, but stays in place, so the next wait is heard. */
|
|
9
11
|
isLoading: boolean;
|
|
10
12
|
/** What is on its way, in pixel caps (`"loading the items"`). `"loading"` by default. */
|
|
11
13
|
label?: string;
|
|
14
|
+
/** Attributes for the line, such as the id and `tabindex="-1"` that make it a place to
|
|
15
|
+
* send focus. */
|
|
16
|
+
attributes?: Attributes<Message>;
|
|
12
17
|
}>;
|
|
13
18
|
|
|
14
19
|
/** What a panel says while its content is on the way, centred where the content will be, with
|
|
15
20
|
* the working cursor. A polite status that stays in place with its words swapped, as a live
|
|
16
21
|
* region must be for a screen reader to hear it: render it before the content, from before
|
|
17
|
-
* the wait starts, and set `isLoading`.
|
|
18
|
-
|
|
19
|
-
|
|
22
|
+
* the wait starts, and set `isLoading`. It is not the page's live region (the Announcer's,
|
|
23
|
+
* marked `data-pixel-paper-announcer`), so a test that counts live regions counts that. */
|
|
24
|
+
export const view = <Message>(config: ViewConfig<Message>, h: HtmlBuilder<Message>): Html => {
|
|
25
|
+
const { isLoading, label = "loading", attributes = [] } = config;
|
|
20
26
|
|
|
21
27
|
return h.div(
|
|
22
|
-
[h.Role("status"), h.Class(isLoading ? "loading" : "loading-idle")],
|
|
28
|
+
[...attributes, h.Role("status"), h.Class(isLoading ? "loading" : "loading-idle")],
|
|
23
29
|
isLoading ? [label, h.span([h.Class("cursor"), h.AriaHidden(true)])] : [],
|
|
24
30
|
);
|
|
25
31
|
};
|
package/src/meter/meter.ts
CHANGED
|
@@ -76,6 +76,9 @@ export type MeterConfig = Readonly<{
|
|
|
76
76
|
/** Replaces the printed numbers and what screen readers hear:
|
|
77
77
|
* `"41,200 of 50,000 requests"`. */
|
|
78
78
|
valueText?: string;
|
|
79
|
+
/** Prints the numbers line (the default). Leave it out where it would repeat the
|
|
80
|
+
* percentage (`valueText: "62% used"`); screen readers still hear `valueText`. */
|
|
81
|
+
hasNumbers?: boolean;
|
|
79
82
|
/** In a red chip once the value reaches `max`. The percentage by default. */
|
|
80
83
|
full?: string;
|
|
81
84
|
/** The last reading failed: the cells print muted, the numbers are the last known, and
|
|
@@ -117,7 +120,16 @@ const percentView = <Message>(
|
|
|
117
120
|
};
|
|
118
121
|
|
|
119
122
|
const meterView = <Message>(config: MeterConfig, h: HtmlBuilder<Message>): Html => {
|
|
120
|
-
const {
|
|
123
|
+
const {
|
|
124
|
+
id,
|
|
125
|
+
label,
|
|
126
|
+
value,
|
|
127
|
+
max,
|
|
128
|
+
detail = [],
|
|
129
|
+
foot = [],
|
|
130
|
+
isStale = false,
|
|
131
|
+
hasNumbers = true,
|
|
132
|
+
} = config;
|
|
121
133
|
const share = max > 0 ? Math.min(1, Math.max(0, value / max)) : 0;
|
|
122
134
|
const isFull = value >= max;
|
|
123
135
|
const numbers = config.valueText ?? numbersText(value, max, config.unit);
|
|
@@ -152,7 +164,7 @@ const meterView = <Message>(config: MeterConfig, h: HtmlBuilder<Message>): Html
|
|
|
152
164
|
cellRowView(METER_CELLS, scaled(share, METER_CELLS, isFull), h),
|
|
153
165
|
),
|
|
154
166
|
percentView(config, scaled(share, PERCENT, isFull), isFull, h),
|
|
155
|
-
h.span([h.Class("meter-num tnum")], [numbers]),
|
|
167
|
+
...(hasNumbers ? [h.span([h.Class("meter-num tnum")], [numbers])] : []),
|
|
156
168
|
],
|
|
157
169
|
),
|
|
158
170
|
...slotView("div", "meter-foot", foot, h),
|
|
@@ -177,6 +189,8 @@ export type ListViewConfig = Readonly<{
|
|
|
177
189
|
*/
|
|
178
190
|
export const listView = <Message>(config: ListViewConfig, h: HtmlBuilder<Message>): Html =>
|
|
179
191
|
h.ul(
|
|
180
|
-
|
|
192
|
+
// NOTE: Safari takes list semantics from a list printed without markers, so the list
|
|
193
|
+
// says it is one
|
|
194
|
+
[h.Role("list"), h.Class("meters")],
|
|
181
195
|
Array.map(config.meters, (meter) => meterView(meter, h)),
|
|
182
196
|
);
|
package/src/pixel-paper.css
CHANGED
|
@@ -32,8 +32,7 @@ body {
|
|
|
32
32
|
line-height: 1.45;
|
|
33
33
|
color: var(--ink);
|
|
34
34
|
caret-color: var(--ink);
|
|
35
|
-
background
|
|
36
|
-
background-image: var(--dither);
|
|
35
|
+
background: var(--dither);
|
|
37
36
|
}
|
|
38
37
|
|
|
39
38
|
fieldset {
|
|
@@ -380,16 +379,20 @@ button.tag > span {
|
|
|
380
379
|
}
|
|
381
380
|
|
|
382
381
|
/* the right-hand tools wrap as one group, never one control per line */
|
|
382
|
+
/* a bar of tools wider than the window wraps onto lines of its own, at the right */
|
|
383
383
|
.topbar-tools {
|
|
384
384
|
display: flex;
|
|
385
|
+
flex-wrap: wrap;
|
|
386
|
+
justify-content: flex-end;
|
|
385
387
|
align-items: center;
|
|
386
|
-
gap: 16px;
|
|
388
|
+
gap: 8px 16px;
|
|
389
|
+
min-width: 0;
|
|
387
390
|
margin-left: auto;
|
|
388
391
|
}
|
|
389
392
|
|
|
390
393
|
@media (max-width: 600px) {
|
|
391
394
|
.topbar-tools {
|
|
392
|
-
gap: 12px;
|
|
395
|
+
gap: 8px 12px;
|
|
393
396
|
}
|
|
394
397
|
|
|
395
398
|
.topbar-tools .btn {
|
|
@@ -515,7 +518,7 @@ a.textbtn {
|
|
|
515
518
|
|
|
516
519
|
/* the phone bar: wordmark and tools on one line, the page nav as a full-width strip
|
|
517
520
|
under them. Where the two do not fit side by side the tools wrap under the wordmark,
|
|
518
|
-
which never clips.
|
|
521
|
+
which never clips. Every tool stays: a session action may have nowhere else to live */
|
|
519
522
|
.topbar {
|
|
520
523
|
gap: 8px 12px;
|
|
521
524
|
padding: 8px 12px;
|
|
@@ -532,11 +535,6 @@ a.textbtn {
|
|
|
532
535
|
text-align: center;
|
|
533
536
|
}
|
|
534
537
|
|
|
535
|
-
.topbar .topbar-tools .textbtn,
|
|
536
|
-
.topbar .link-state:not(.offline, .reconnecting) {
|
|
537
|
-
display: none;
|
|
538
|
-
}
|
|
539
|
-
|
|
540
538
|
.deck {
|
|
541
539
|
padding: 20px 12px 64px;
|
|
542
540
|
}
|
|
@@ -2634,8 +2632,7 @@ dialog.veil:focus {
|
|
|
2634
2632
|
.veil-backdrop {
|
|
2635
2633
|
position: fixed;
|
|
2636
2634
|
inset: 0;
|
|
2637
|
-
background
|
|
2638
|
-
background-image: var(--veil-grain);
|
|
2635
|
+
background: var(--veil-grain), var(--veil);
|
|
2639
2636
|
animation: veilIn var(--motion-veil) steps(3, end);
|
|
2640
2637
|
}
|
|
2641
2638
|
|
|
@@ -3329,6 +3326,143 @@ dialog.veil:focus {
|
|
|
3329
3326
|
flex: 1;
|
|
3330
3327
|
}
|
|
3331
3328
|
|
|
3329
|
+
/* ---------- sortable: items the reader puts in order, as rows or tiles ---------- */
|
|
3330
|
+
|
|
3331
|
+
.sortable-items {
|
|
3332
|
+
list-style: none;
|
|
3333
|
+
}
|
|
3334
|
+
|
|
3335
|
+
/* a row: the grip, then the item, between 1px rules, edge to edge in a flush panel */
|
|
3336
|
+
.sortable-item {
|
|
3337
|
+
display: flex;
|
|
3338
|
+
align-items: center;
|
|
3339
|
+
gap: 10px;
|
|
3340
|
+
min-width: 0;
|
|
3341
|
+
padding: 8px var(--pad) 8px 10px;
|
|
3342
|
+
position: relative;
|
|
3343
|
+
background: var(--paper);
|
|
3344
|
+
border-bottom: var(--hair) solid var(--rule);
|
|
3345
|
+
}
|
|
3346
|
+
|
|
3347
|
+
.sortable-items > .sortable-item:last-child {
|
|
3348
|
+
border-bottom: 0;
|
|
3349
|
+
}
|
|
3350
|
+
|
|
3351
|
+
.sortable-body {
|
|
3352
|
+
flex: 1;
|
|
3353
|
+
min-width: 0;
|
|
3354
|
+
}
|
|
3355
|
+
|
|
3356
|
+
.sortable-item.movable {
|
|
3357
|
+
cursor: grab;
|
|
3358
|
+
}
|
|
3359
|
+
|
|
3360
|
+
.sortable-item [data-sortable-grip] {
|
|
3361
|
+
flex: none;
|
|
3362
|
+
}
|
|
3363
|
+
|
|
3364
|
+
.sortable-item.movable [data-sortable-grip] {
|
|
3365
|
+
cursor: grab;
|
|
3366
|
+
/* the grip is where a finger drags; the rest of the item scrolls the page */
|
|
3367
|
+
touch-action: none;
|
|
3368
|
+
}
|
|
3369
|
+
|
|
3370
|
+
.sortable-item.movable:hover {
|
|
3371
|
+
background: var(--head);
|
|
3372
|
+
}
|
|
3373
|
+
|
|
3374
|
+
.sortable-item.drag-dim {
|
|
3375
|
+
opacity: 0.4;
|
|
3376
|
+
}
|
|
3377
|
+
|
|
3378
|
+
/* where a pointer would drop the held item: a 2px dashed ink line beside the item it lands
|
|
3379
|
+
next to, drawn over the layout so nothing moves under the pointer */
|
|
3380
|
+
.sortable-item:is(.drop-before, .drop-after)::before {
|
|
3381
|
+
content: "";
|
|
3382
|
+
position: absolute;
|
|
3383
|
+
left: 0;
|
|
3384
|
+
right: 0;
|
|
3385
|
+
border-top: var(--line) dashed var(--ink);
|
|
3386
|
+
pointer-events: none;
|
|
3387
|
+
}
|
|
3388
|
+
|
|
3389
|
+
.sortable-item.drop-before::before {
|
|
3390
|
+
top: -1px;
|
|
3391
|
+
}
|
|
3392
|
+
|
|
3393
|
+
.sortable-item.drop-after::before {
|
|
3394
|
+
bottom: -1px;
|
|
3395
|
+
}
|
|
3396
|
+
|
|
3397
|
+
.sortable.grid .sortable-item:is(.drop-before, .drop-after)::before {
|
|
3398
|
+
top: -2px;
|
|
3399
|
+
bottom: -2px;
|
|
3400
|
+
left: auto;
|
|
3401
|
+
right: auto;
|
|
3402
|
+
border-top: 0;
|
|
3403
|
+
border-left: var(--line) dashed var(--ink);
|
|
3404
|
+
}
|
|
3405
|
+
|
|
3406
|
+
.sortable.grid .sortable-item.drop-before::before {
|
|
3407
|
+
left: -9px;
|
|
3408
|
+
}
|
|
3409
|
+
|
|
3410
|
+
.sortable.grid .sortable-item.drop-after::before {
|
|
3411
|
+
right: -9px;
|
|
3412
|
+
}
|
|
3413
|
+
|
|
3414
|
+
/* the held copy, following the pointer or where the keyboard would drop it: lifted onto the
|
|
3415
|
+
card shadow and tinted, as a board's is; the held item keeps its own print */
|
|
3416
|
+
.sortable-item.lifted {
|
|
3417
|
+
border: var(--line) solid var(--ink);
|
|
3418
|
+
background: var(--head);
|
|
3419
|
+
box-shadow: var(--shadow-card);
|
|
3420
|
+
cursor: grabbing;
|
|
3421
|
+
}
|
|
3422
|
+
|
|
3423
|
+
.sortable-slot {
|
|
3424
|
+
list-style: none;
|
|
3425
|
+
padding: 6px;
|
|
3426
|
+
}
|
|
3427
|
+
|
|
3428
|
+
.sortable-lift {
|
|
3429
|
+
width: var(--lift-width, 320px);
|
|
3430
|
+
translate: var(--lift-x, 0) var(--lift-y, 0);
|
|
3431
|
+
}
|
|
3432
|
+
|
|
3433
|
+
/* tiles: paper slips on the 3px print shadow that wrap, 180px and up */
|
|
3434
|
+
.sortable.grid .sortable-items {
|
|
3435
|
+
display: grid;
|
|
3436
|
+
grid-template-columns: repeat(auto-fill, minmax(180px, 1fr));
|
|
3437
|
+
gap: 12px;
|
|
3438
|
+
padding: 12px;
|
|
3439
|
+
}
|
|
3440
|
+
|
|
3441
|
+
.sortable.grid .sortable-item {
|
|
3442
|
+
align-items: flex-start;
|
|
3443
|
+
height: 100%;
|
|
3444
|
+
padding: 9px 11px 9px 6px;
|
|
3445
|
+
border: var(--line) solid var(--ink);
|
|
3446
|
+
box-shadow: var(--shadow-button);
|
|
3447
|
+
}
|
|
3448
|
+
|
|
3449
|
+
.sortable.grid .sortable-item.lifted {
|
|
3450
|
+
box-shadow: var(--shadow-card);
|
|
3451
|
+
}
|
|
3452
|
+
|
|
3453
|
+
.sortable.grid .sortable-slot {
|
|
3454
|
+
padding: 0;
|
|
3455
|
+
}
|
|
3456
|
+
|
|
3457
|
+
/* while a pointer carries an item, the page grabs and selects nothing */
|
|
3458
|
+
:root[data-pixel-paper-dragging] {
|
|
3459
|
+
user-select: none;
|
|
3460
|
+
}
|
|
3461
|
+
|
|
3462
|
+
:root[data-pixel-paper-dragging] * {
|
|
3463
|
+
cursor: grabbing !important;
|
|
3464
|
+
}
|
|
3465
|
+
|
|
3332
3466
|
/* ---------- cells: a reading printed as ink squares in a box ---------- */
|
|
3333
3467
|
|
|
3334
3468
|
.cells {
|
|
@@ -3355,15 +3489,19 @@ dialog.veil:focus {
|
|
|
3355
3489
|
height: 8px;
|
|
3356
3490
|
}
|
|
3357
3491
|
|
|
3492
|
+
/* meters in a list step down by their column instead (below) */
|
|
3358
3493
|
@media (max-width: 600px) {
|
|
3359
|
-
.cells:not(.short) .cell {
|
|
3494
|
+
.cells:not(.short, .meters *) .cell {
|
|
3360
3495
|
width: 8px;
|
|
3361
3496
|
}
|
|
3362
3497
|
}
|
|
3363
3498
|
|
|
3364
3499
|
/* ---------- meters: a named reading, its cells, the numbers and a foot line ---------- */
|
|
3365
3500
|
|
|
3501
|
+
/* the list is the container its rows measure: they stack and their cells narrow by the
|
|
3502
|
+
column they are placed in, not by the window */
|
|
3366
3503
|
.meters {
|
|
3504
|
+
container: meters / inline-size;
|
|
3367
3505
|
list-style: none;
|
|
3368
3506
|
}
|
|
3369
3507
|
|
|
@@ -3393,7 +3531,7 @@ dialog.veil:focus {
|
|
|
3393
3531
|
align-items: center;
|
|
3394
3532
|
}
|
|
3395
3533
|
|
|
3396
|
-
@
|
|
3534
|
+
@container meters (width < 560px) {
|
|
3397
3535
|
.meter-row {
|
|
3398
3536
|
grid-template-columns: minmax(0, 1fr);
|
|
3399
3537
|
}
|
|
@@ -3401,12 +3539,38 @@ dialog.veil:focus {
|
|
|
3401
3539
|
.meter-head {
|
|
3402
3540
|
grid-row: auto;
|
|
3403
3541
|
flex-direction: row;
|
|
3542
|
+
flex-wrap: wrap;
|
|
3404
3543
|
align-items: baseline;
|
|
3405
|
-
gap: 12px;
|
|
3544
|
+
gap: 2px 12px;
|
|
3545
|
+
}
|
|
3546
|
+
|
|
3547
|
+
.meter-num {
|
|
3548
|
+
width: 100%;
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
|
|
3552
|
+
/* twenty cells take 246px at 10px each; in a narrower column they step down whole pixels,
|
|
3553
|
+
never to a fraction, so a meter fits a column of 160px and up */
|
|
3554
|
+
@container meters (width < 280px) {
|
|
3555
|
+
.meters .cells .cell {
|
|
3556
|
+
width: 8px;
|
|
3557
|
+
}
|
|
3558
|
+
}
|
|
3559
|
+
|
|
3560
|
+
@container meters (width < 240px) {
|
|
3561
|
+
.meters .cells .cell {
|
|
3562
|
+
width: 6px;
|
|
3563
|
+
}
|
|
3564
|
+
}
|
|
3565
|
+
|
|
3566
|
+
@container meters (width < 200px) {
|
|
3567
|
+
.meters .cells .cell {
|
|
3568
|
+
width: 4px;
|
|
3406
3569
|
}
|
|
3407
3570
|
}
|
|
3408
3571
|
|
|
3409
3572
|
.meter-name {
|
|
3573
|
+
overflow-wrap: anywhere;
|
|
3410
3574
|
font-family: var(--font-px);
|
|
3411
3575
|
font-size: var(--type-name);
|
|
3412
3576
|
text-transform: uppercase;
|
|
@@ -3462,12 +3626,6 @@ dialog.veil:focus {
|
|
|
3462
3626
|
color: var(--red);
|
|
3463
3627
|
}
|
|
3464
3628
|
|
|
3465
|
-
@media (max-width: 600px) {
|
|
3466
|
-
.meter-num {
|
|
3467
|
-
width: 100%;
|
|
3468
|
-
}
|
|
3469
|
-
}
|
|
3470
|
-
|
|
3471
3629
|
/* a history list of checks: when, what was checked, what it read */
|
|
3472
3630
|
.history-list.checks {
|
|
3473
3631
|
border-top: 0;
|
|
@@ -4760,16 +4918,14 @@ dialog.veil:focus {
|
|
|
4760
4918
|
height: 100dvh;
|
|
4761
4919
|
max-height: none;
|
|
4762
4920
|
padding: 12px 16px var(--gap) 12px;
|
|
4763
|
-
background
|
|
4764
|
-
background-image: var(--dither);
|
|
4921
|
+
background: var(--dither);
|
|
4765
4922
|
border-right: var(--line) solid var(--ink);
|
|
4766
4923
|
box-shadow: var(--shadow-card);
|
|
4767
4924
|
animation: railIn var(--motion-card) steps(3, end);
|
|
4768
4925
|
}
|
|
4769
4926
|
|
|
4770
4927
|
.rail[popover]::backdrop {
|
|
4771
|
-
background
|
|
4772
|
-
background-image: var(--veil-grain);
|
|
4928
|
+
background: var(--veil-grain), var(--veil);
|
|
4773
4929
|
}
|
|
4774
4930
|
|
|
4775
4931
|
.rail-head {
|
|
@@ -4840,8 +4996,7 @@ dialog.veil:focus {
|
|
|
4840
4996
|
display: grid;
|
|
4841
4997
|
place-items: center;
|
|
4842
4998
|
padding: 24px;
|
|
4843
|
-
background
|
|
4844
|
-
background-image: var(--veil-grain);
|
|
4999
|
+
background: var(--veil-grain), var(--veil);
|
|
4845
5000
|
}
|
|
4846
5001
|
|
|
4847
5002
|
.stage-paused > .notice {
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { AsyncData } from "foldkit";
|
|
2
|
+
import type { Html, HtmlBuilder } from "foldkit/html";
|
|
3
|
+
|
|
4
|
+
import * as Button from "../button/button.ts";
|
|
5
|
+
import * as Loading from "../loading/loading.ts";
|
|
6
|
+
import * as Notice from "../notice/notice.ts";
|
|
7
|
+
|
|
8
|
+
// VIEW
|
|
9
|
+
|
|
10
|
+
/** Configuration for rendering data that loads with `view`. */
|
|
11
|
+
export type ViewConfig<Data, Error, Message> = Readonly<{
|
|
12
|
+
/** The loading line's id, unique in the document. Retry takes its button away with the
|
|
13
|
+
* notice, so return `Focus.Focus({ id })` with the retry: focus lands on the line, which
|
|
14
|
+
* says a first load and stays quiet over a refresh. */
|
|
15
|
+
id: string;
|
|
16
|
+
data: AsyncData.AsyncData<Data, Error>;
|
|
17
|
+
/** What loads, as a noun in the words around it: `"usage"` reads "loading usage", "Usage
|
|
18
|
+
* not loaded" and "Usage not refreshed". */
|
|
19
|
+
label: string;
|
|
20
|
+
/** Why a load failed, in a sentence, under the notice's title. */
|
|
21
|
+
toErrorText: (error: Error) => string;
|
|
22
|
+
/** Loads it again: the notice's retry button, on a failure and on stale data alike. */
|
|
23
|
+
onRetry: Message;
|
|
24
|
+
/** The data, loaded or the last loaded (an EmptyState for none is the caller's). */
|
|
25
|
+
toDataView: (data: Data) => ReadonlyArray<Html>;
|
|
26
|
+
}>;
|
|
27
|
+
|
|
28
|
+
const capitalised = (label: string): string => `${label.charAt(0).toUpperCase()}${label.slice(1)}`;
|
|
29
|
+
|
|
30
|
+
/**
|
|
31
|
+
* Data that loads, in each of Foldkit's `AsyncData` states, with the retry built in: a Loading
|
|
32
|
+
* line while it first loads (in place in every state, so its status is heard), an error
|
|
33
|
+
* Notice with retry when it failed with nothing to show, a warn Notice with retry above the
|
|
34
|
+
* last data when a refresh failed (stale), and the data itself once loaded or while it
|
|
35
|
+
* refreshes, quietly, so a page that polls is not heard each time. Spread it where the
|
|
36
|
+
* content goes.
|
|
37
|
+
*/
|
|
38
|
+
export const view = <Data, Error, Message>(
|
|
39
|
+
config: ViewConfig<Data, Error, Message>,
|
|
40
|
+
h: HtmlBuilder<Message>,
|
|
41
|
+
): ReadonlyArray<Html> => {
|
|
42
|
+
const { id, data, label, toErrorText, onRetry, toDataView } = config;
|
|
43
|
+
|
|
44
|
+
const noticeView = (tone: "Error" | "Warn", title: string, error: Error): Html =>
|
|
45
|
+
Notice.view(
|
|
46
|
+
{
|
|
47
|
+
tone,
|
|
48
|
+
title,
|
|
49
|
+
body: [toErrorText(error)],
|
|
50
|
+
actions: [Button.view({ label: "retry", isSmall: true, onClick: onRetry }, h)],
|
|
51
|
+
},
|
|
52
|
+
h,
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return [
|
|
56
|
+
Loading.view(
|
|
57
|
+
{
|
|
58
|
+
isLoading: AsyncData.isLoading(data),
|
|
59
|
+
label: `loading ${label}`,
|
|
60
|
+
attributes: [h.Id(id), h.Tabindex(-1)],
|
|
61
|
+
},
|
|
62
|
+
h,
|
|
63
|
+
),
|
|
64
|
+
...AsyncData.match(data, {
|
|
65
|
+
onIdle: () => [],
|
|
66
|
+
onLoading: () => [],
|
|
67
|
+
onFailure: (error) => [noticeView("Error", `${capitalised(label)} not loaded`, error)],
|
|
68
|
+
onStale: ({ error, data: lastData }) => [
|
|
69
|
+
noticeView("Warn", `${capitalised(label)} not refreshed`, error),
|
|
70
|
+
...toDataView(lastData),
|
|
71
|
+
],
|
|
72
|
+
onRefreshing: toDataView,
|
|
73
|
+
onSuccess: toDataView,
|
|
74
|
+
}),
|
|
75
|
+
];
|
|
76
|
+
};
|
package/src/skipLink/index.ts
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
export {
|
|
1
|
+
export { ScrollToFragment, view } from "./skipLink.ts";
|