@maple-kit/ui 0.1.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/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +39 -0
- package/dist/composer/actions.d.ts +15 -0
- package/dist/composer/actions.js +59 -0
- package/dist/composer/attachments.d.ts +33 -0
- package/dist/composer/attachments.js +166 -0
- package/dist/composer/badge.d.ts +13 -0
- package/dist/composer/badge.js +25 -0
- package/dist/composer/body.d.ts +13 -0
- package/dist/composer/body.js +119 -0
- package/dist/composer/composer.d.ts +23 -0
- package/dist/composer/composer.js +122 -0
- package/dist/composer/css.js +559 -0
- package/dist/composer/detail.d.ts +32 -0
- package/dist/composer/detail.js +102 -0
- package/dist/composer/emoji.d.ts +54 -0
- package/dist/composer/emoji.js +131 -0
- package/dist/composer/index.d.ts +15 -0
- package/dist/composer/index.js +15 -0
- package/dist/composer/inset.js +20 -0
- package/dist/composer/leave.d.ts +19 -0
- package/dist/composer/leave.js +34 -0
- package/dist/composer/peek.d.ts +21 -0
- package/dist/composer/peek.js +29 -0
- package/dist/composer/phrase.d.ts +27 -0
- package/dist/composer/phrase.js +30 -0
- package/dist/composer/picker-emoji.d.ts +34 -0
- package/dist/composer/picker-emoji.js +48 -0
- package/dist/composer/prompt.js +31 -0
- package/dist/composer/scope.d.ts +11 -0
- package/dist/composer/scope.js +16 -0
- package/dist/composer/target.d.ts +13 -0
- package/dist/composer/target.js +45 -0
- package/dist/context.d.ts +21 -0
- package/dist/context.js +16 -0
- package/dist/data.d.ts +52 -0
- package/dist/data.js +30 -0
- package/dist/icons/branch.d.ts +4 -0
- package/dist/icons/branch.js +8 -0
- package/dist/icons/cog.d.ts +4 -0
- package/dist/icons/cog.js +8 -0
- package/dist/icons/commit.d.ts +4 -0
- package/dist/icons/commit.js +5 -0
- package/dist/icons/crossfade.d.ts +15 -0
- package/dist/icons/crossfade.js +27 -0
- package/dist/icons/element.d.ts +4 -0
- package/dist/icons/element.js +5 -0
- package/dist/icons/icon.d.ts +24 -0
- package/dist/icons/icon.js +27 -0
- package/dist/icons/index.d.ts +13 -0
- package/dist/icons/index.js +13 -0
- package/dist/icons/noplace.d.ts +4 -0
- package/dist/icons/noplace.js +5 -0
- package/dist/icons/region.d.ts +4 -0
- package/dist/icons/region.js +8 -0
- package/dist/icons/smiley.d.ts +4 -0
- package/dist/icons/smiley.js +8 -0
- package/dist/icons/sparkle.d.ts +4 -0
- package/dist/icons/sparkle.js +8 -0
- package/dist/icons/target.d.ts +4 -0
- package/dist/icons/target.js +5 -0
- package/dist/icons/text.d.ts +4 -0
- package/dist/icons/text.js +5 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/island/account.d.ts +10 -0
- package/dist/island/account.js +52 -0
- package/dist/island/comments.d.ts +24 -0
- package/dist/island/comments.js +40 -0
- package/dist/island/content.d.ts +10 -0
- package/dist/island/content.js +26 -0
- package/dist/island/context.d.ts +58 -0
- package/dist/island/context.js +23 -0
- package/dist/island/css.d.ts +13 -0
- package/dist/island/css.js +1020 -0
- package/dist/island/drag.d.ts +10 -0
- package/dist/island/drag.js +77 -0
- package/dist/island/filters.d.ts +10 -0
- package/dist/island/filters.js +63 -0
- package/dist/island/header.d.ts +32 -0
- package/dist/island/header.js +47 -0
- package/dist/island/index.d.ts +18 -0
- package/dist/island/index.js +18 -0
- package/dist/island/island.d.ts +12 -0
- package/dist/island/island.js +111 -0
- package/dist/island/item.d.ts +12 -0
- package/dist/island/item.js +158 -0
- package/dist/island/language.d.ts +55 -0
- package/dist/island/language.js +125 -0
- package/dist/island/leaf.js +17 -0
- package/dist/island/list.d.ts +12 -0
- package/dist/island/list.js +31 -0
- package/dist/island/new-comment.d.ts +18 -0
- package/dist/island/new-comment.js +56 -0
- package/dist/island/settings.d.ts +10 -0
- package/dist/island/settings.js +134 -0
- package/dist/island/stagger.d.ts +12 -0
- package/dist/island/stagger.js +4 -0
- package/dist/island/time.d.ts +17 -0
- package/dist/island/time.js +29 -0
- package/dist/island/trigger.d.ts +10 -0
- package/dist/island/trigger.js +39 -0
- package/dist/language.d.ts +45 -0
- package/dist/language.js +66 -0
- package/dist/maple.d.ts +25 -0
- package/dist/maple.js +65 -0
- package/dist/marks/avatar.d.ts +16 -0
- package/dist/marks/avatar.js +36 -0
- package/dist/marks/css.d.ts +12 -0
- package/dist/marks/css.js +371 -0
- package/dist/marks/frame.d.ts +25 -0
- package/dist/marks/frame.js +53 -0
- package/dist/marks/geometry.d.ts +55 -0
- package/dist/marks/geometry.js +61 -0
- package/dist/marks/index.d.ts +14 -0
- package/dist/marks/index.js +14 -0
- package/dist/marks/label.d.ts +23 -0
- package/dist/marks/label.js +31 -0
- package/dist/marks/layer.d.ts +16 -0
- package/dist/marks/layer.js +194 -0
- package/dist/marks/leaf.d.ts +18 -0
- package/dist/marks/leaf.js +7 -0
- package/dist/marks/mark.d.ts +28 -0
- package/dist/marks/mark.js +43 -0
- package/dist/marks/nudge.d.ts +50 -0
- package/dist/marks/nudge.js +76 -0
- package/dist/marks/paint.d.ts +11 -0
- package/dist/marks/paint.js +19 -0
- package/dist/marks/placement.d.ts +24 -0
- package/dist/marks/placement.js +28 -0
- package/dist/marks/ring.d.ts +28 -0
- package/dist/marks/ring.js +85 -0
- package/dist/marks/shape.d.ts +21 -0
- package/dist/marks/shape.js +51 -0
- package/dist/notice/css.d.ts +11 -0
- package/dist/notice/css.js +74 -0
- package/dist/notice/index.d.ts +4 -0
- package/dist/notice/index.js +4 -0
- package/dist/notice/language.d.ts +15 -0
- package/dist/notice/language.js +14 -0
- package/dist/notice/notice.d.ts +14 -0
- package/dist/notice/notice.js +49 -0
- package/dist/part.d.ts +9 -0
- package/dist/part.js +11 -0
- package/dist/picker/css.d.ts +13 -0
- package/dist/picker/css.js +141 -0
- package/dist/picker/index.d.ts +6 -0
- package/dist/picker/index.js +6 -0
- package/dist/picker/language.d.ts +14 -0
- package/dist/picker/language.js +17 -0
- package/dist/picker/picker.d.ts +18 -0
- package/dist/picker/picker.js +164 -0
- package/dist/picker/session.d.ts +16 -0
- package/dist/picker/session.js +35 -0
- package/dist/picker/target.d.ts +23 -0
- package/dist/picker/target.js +37 -0
- package/dist/root.d.ts +42 -0
- package/dist/root.js +75 -0
- package/dist/shots.js +32 -0
- package/dist/slot.d.ts +28 -0
- package/dist/slot.js +52 -0
- package/dist/slots.d.ts +24 -0
- package/dist/slots.js +75 -0
- package/dist/stylesheet.d.ts +15 -0
- package/dist/stylesheet.js +188 -0
- package/dist/theme.d.ts +20 -0
- package/dist/theme.js +13 -0
- package/dist/tip.d.ts +40 -0
- package/dist/tip.js +82 -0
- package/dist/tokens.d.ts +28 -0
- package/dist/tokens.js +180 -0
- package/package.json +56 -0
package/dist/maple.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
import { MapleRoot } from "./root.js";
|
|
2
|
+
import { MapleActions } from "./composer/actions.js";
|
|
3
|
+
import { MapleAttachments } from "./composer/attachments.js";
|
|
4
|
+
import { MapleContextBadge } from "./composer/badge.js";
|
|
5
|
+
import { MapleBody } from "./composer/body.js";
|
|
6
|
+
import { MapleComposer } from "./composer/composer.js";
|
|
7
|
+
import { MapleDetail } from "./composer/detail.js";
|
|
8
|
+
import { MapleTarget } from "./composer/target.js";
|
|
9
|
+
import { PICK_ORDER } from "./island/language.js";
|
|
10
|
+
import { IslandContent } from "./island/content.js";
|
|
11
|
+
import { Filters } from "./island/filters.js";
|
|
12
|
+
import { Branch, Header, Logo } from "./island/header.js";
|
|
13
|
+
import { Island } from "./island/island.js";
|
|
14
|
+
import { Item } from "./island/item.js";
|
|
15
|
+
import { List } from "./island/list.js";
|
|
16
|
+
import { NewComment, PickButton } from "./island/new-comment.js";
|
|
17
|
+
import { Settings } from "./island/settings.js";
|
|
18
|
+
import { IslandTrigger } from "./island/trigger.js";
|
|
19
|
+
import { MapleMarkLayer } from "./marks/layer.js";
|
|
20
|
+
import { MapleNotice } from "./notice/notice.js";
|
|
21
|
+
import { MaplePicker } from "./picker/picker.js";
|
|
22
|
+
import { createElement, forwardRef } from "react";
|
|
23
|
+
//#region src/maple.ts
|
|
24
|
+
const LOAD_CALLS = [
|
|
25
|
+
"load",
|
|
26
|
+
"status",
|
|
27
|
+
"link"
|
|
28
|
+
];
|
|
29
|
+
const SEND_CALLS = ["send"];
|
|
30
|
+
const Maple = forwardRef(function Maple(props, ref) {
|
|
31
|
+
const { attachments, children, defaultOpen, hint, leave, ...root } = props;
|
|
32
|
+
return createElement(MapleRoot, {
|
|
33
|
+
...root,
|
|
34
|
+
ref
|
|
35
|
+
}, createElement(MapleMarkLayer, { key: "marks" }), createElement(MaplePicker, {
|
|
36
|
+
key: "picker",
|
|
37
|
+
...hint === void 0 ? {} : { hint }
|
|
38
|
+
}), inventory(root.branch, root.label, defaultOpen === true), composer(leave, attachments), children);
|
|
39
|
+
});
|
|
40
|
+
function inventory(branch, label, defaultOpen) {
|
|
41
|
+
return createElement(Island, {
|
|
42
|
+
defaultOpen,
|
|
43
|
+
key: "island"
|
|
44
|
+
}, createElement(IslandTrigger), createElement(IslandContent, null, createElement(Header, null, createElement(Logo), createElement(Branch, {
|
|
45
|
+
branch,
|
|
46
|
+
...label === void 0 ? {} : { label }
|
|
47
|
+
}), createElement(Settings)), createElement(MapleNotice, { during: LOAD_CALLS }), createElement(Filters), createElement(List, { children: (comment) => createElement(Item, { comment }) }), createElement(NewComment, null, ...PICK_ORDER.map((kind) => createElement(PickButton, {
|
|
48
|
+
key: kind,
|
|
49
|
+
kind
|
|
50
|
+
})))));
|
|
51
|
+
}
|
|
52
|
+
function composer(leave, shots) {
|
|
53
|
+
return createElement(MapleComposer, {
|
|
54
|
+
key: "composer",
|
|
55
|
+
...leave === void 0 ? {} : { leave }
|
|
56
|
+
}, createElement(MapleTarget, { key: "target" }), createElement(MapleBody, { key: "body" }), createElement(MapleDetail, { key: "detail" }), createElement(MapleContextBadge, { key: "context" }), createElement(MapleAttachments, {
|
|
57
|
+
key: "attachments",
|
|
58
|
+
...shots
|
|
59
|
+
}), createElement(MapleNotice, {
|
|
60
|
+
key: "notice",
|
|
61
|
+
during: SEND_CALLS
|
|
62
|
+
}), createElement(MapleActions, { key: "actions" }));
|
|
63
|
+
}
|
|
64
|
+
//#endregion
|
|
65
|
+
export { Maple };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { AsChildProps } from "../slot.js";
|
|
2
|
+
import { HTMLAttributes } from "react";
|
|
3
|
+
import { IdentityProvenance } from "@maple-kit/core";
|
|
4
|
+
//#region src/marks/avatar.d.ts
|
|
5
|
+
/** Who wrote it, in their own colour. */
|
|
6
|
+
export interface AvatarProps extends AsChildProps, HTMLAttributes<HTMLSpanElement> {
|
|
7
|
+
readonly name: string;
|
|
8
|
+
readonly provenance?: IdentityProvenance;
|
|
9
|
+
/** The reviewer colour, 0 to 9. */
|
|
10
|
+
readonly colorSlot?: number;
|
|
11
|
+
}
|
|
12
|
+
/** The first letters of the first two words, which is what a name reduces to. */
|
|
13
|
+
export declare function initialsOf(name: string): string;
|
|
14
|
+
/** An author's leaf. The provenance is the fill; the name is beside it. */
|
|
15
|
+
export declare const MapleAvatar: import("react").ForwardRefExoticComponent<AvatarProps & import("react").RefAttributes<HTMLSpanElement>>;
|
|
16
|
+
//#endregion
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { dataAttributes } from "../data.js";
|
|
2
|
+
import { Slot, composeRefs } from "../slot.js";
|
|
3
|
+
import { applyReviewerSlot } from "../slots.js";
|
|
4
|
+
import { MapleLeaf, inside } from "./shape.js";
|
|
5
|
+
import { createElement, forwardRef, useCallback } from "react";
|
|
6
|
+
//#region src/marks/avatar.ts
|
|
7
|
+
const TOOLTIPS = {
|
|
8
|
+
server: "verified by the application's own session",
|
|
9
|
+
client: "the application told us; unverified",
|
|
10
|
+
guest: "typed a name into Maple"
|
|
11
|
+
};
|
|
12
|
+
function initialsOf(name) {
|
|
13
|
+
return name.split(/\s+/).filter(Boolean).slice(0, 2).map((word) => [...word][0]?.toUpperCase() ?? "").join("");
|
|
14
|
+
}
|
|
15
|
+
const MapleAvatar = forwardRef(function MapleAvatar(props, ref) {
|
|
16
|
+
const { asChild, children, className, colorSlot, name, provenance = "server", ...rest } = props;
|
|
17
|
+
const paint = useCallback((node) => {
|
|
18
|
+
if (node && colorSlot !== void 0) applyReviewerSlot(node, colorSlot);
|
|
19
|
+
}, [colorSlot]);
|
|
20
|
+
return createElement(asChild ? Slot : "span", {
|
|
21
|
+
...rest,
|
|
22
|
+
...dataAttributes({ provenance }),
|
|
23
|
+
title: `${name} — ${TOOLTIPS[provenance]}`,
|
|
24
|
+
className: className ? `mk-avatar ${className}` : "mk-avatar",
|
|
25
|
+
ref: composeRefs(ref, paint)
|
|
26
|
+
}, ...inside(asChild, children, [createElement(MapleLeaf, {
|
|
27
|
+
key: "leaf",
|
|
28
|
+
form: provenance === "guest" ? "outline" : "solid",
|
|
29
|
+
halo: false
|
|
30
|
+
}), createElement("span", {
|
|
31
|
+
key: "ini",
|
|
32
|
+
className: "mk-avatar-ini"
|
|
33
|
+
}, initialsOf(name))]));
|
|
34
|
+
});
|
|
35
|
+
//#endregion
|
|
36
|
+
export { MapleAvatar, initialsOf };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
//#region src/marks/css.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The marks' half of the adopted stylesheet.
|
|
4
|
+
*
|
|
5
|
+
* It is its own module so `src/stylesheet.ts` composes rather than grows, and
|
|
6
|
+
* so this file is the one place a mark's look is decided. Every value here is
|
|
7
|
+
* a token: a rule that spelled its own duration would stop honouring reduced
|
|
8
|
+
* motion the moment a reviewer asked for it.
|
|
9
|
+
*/
|
|
10
|
+
/** Everything the marks add to the overlay's one stylesheet. */
|
|
11
|
+
export declare function marksCss(): string;
|
|
12
|
+
//#endregion
|
|
@@ -0,0 +1,371 @@
|
|
|
1
|
+
//#region src/marks/css.ts
|
|
2
|
+
const POSITIONED = `
|
|
3
|
+
position: absolute;
|
|
4
|
+
top: 0;
|
|
5
|
+
left: 0;
|
|
6
|
+
translate: var(--mk-x) var(--mk-y);
|
|
7
|
+
`.trim();
|
|
8
|
+
function paintCss() {
|
|
9
|
+
return `
|
|
10
|
+
/* A mark is drawn in its status, not in its author's hue: where a comment is
|
|
11
|
+
in its life is what a reviewer scans the page for, and who wrote it is the
|
|
12
|
+
name beside it. The row's leaf answers to the same four rules, because a
|
|
13
|
+
comment drawn two ways in two places is two comments. */
|
|
14
|
+
:is(.mk-mark, .mk-rowleaf) {
|
|
15
|
+
--mk-paint: var(--mk-pin, var(--mk-accent));
|
|
16
|
+
--mk-ink: var(--mk-pin-ink, var(--mk-accent-ink));
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
.mk-avatar {
|
|
20
|
+
--mk-paint: var(--mk-pin, var(--mk-slot, var(--mk-accent)));
|
|
21
|
+
--mk-ink: var(--mk-pin-ink, var(--mk-slot-ink, var(--mk-accent-ink)));
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
:is(.mk-mark, .mk-rowleaf)[data-status="needs_reverify"] {
|
|
25
|
+
--mk-paint: var(--mk-pin, var(--mk-warn));
|
|
26
|
+
--mk-ink: var(--mk-pin-ink, var(--mk-warn-sub));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
:is(.mk-mark, .mk-rowleaf)[data-status="resolved"] {
|
|
30
|
+
--mk-paint: var(--mk-pin, var(--mk-ok));
|
|
31
|
+
--mk-ink: var(--mk-pin-ink, var(--mk-bg));
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/* Held back, not faded out: at 0.6 a resolved mark read as one that had
|
|
35
|
+
failed to load, and a reviewer re-checking it has to be able to see it. */
|
|
36
|
+
.mk-mark[data-status="resolved"] {
|
|
37
|
+
opacity: 0.85;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
/* Unpinned never reached the page and unsent never left it, so neither takes
|
|
41
|
+
a status colour: grey is what separates them from an open comment, which
|
|
42
|
+
is drawn in the same outline. */
|
|
43
|
+
:is(.mk-mark, .mk-rowleaf)[data-status="orphaned"],
|
|
44
|
+
:is(.mk-mark, .mk-rowleaf)[data-sent="false"] {
|
|
45
|
+
--mk-paint: var(--mk-pin, var(--mk-muted));
|
|
46
|
+
--mk-ink: var(--mk-pin-ink, var(--mk-muted));
|
|
47
|
+
}
|
|
48
|
+
`;
|
|
49
|
+
}
|
|
50
|
+
function leafCss() {
|
|
51
|
+
return `
|
|
52
|
+
.mk-leaf {
|
|
53
|
+
position: absolute;
|
|
54
|
+
inset: 0;
|
|
55
|
+
width: 100%;
|
|
56
|
+
height: 100%;
|
|
57
|
+
overflow: visible;
|
|
58
|
+
pointer-events: none;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
.mk-leaf-halo {
|
|
62
|
+
fill: var(--mk-bg);
|
|
63
|
+
stroke: var(--mk-bg);
|
|
64
|
+
stroke-width: 8;
|
|
65
|
+
stroke-linejoin: round;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
.mk-leaf-body {
|
|
69
|
+
fill: var(--mk-paint);
|
|
70
|
+
stroke: var(--mk-paint);
|
|
71
|
+
stroke-width: 1.5;
|
|
72
|
+
stroke-linejoin: round;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/* The ring is a filled shape carrying its own counter, so it keeps one weight
|
|
76
|
+
at every size and recolours with one declaration. A dashed stroke was tried
|
|
77
|
+
and reads as a broken leaf rather than an empty one, which is a different
|
|
78
|
+
sentence about the comment. The stroke on top of the fill is what gives it
|
|
79
|
+
weight: the drawn path alone is a hairline beside the solid form. */
|
|
80
|
+
.mk-leaf-edge {
|
|
81
|
+
fill: var(--mk-paint);
|
|
82
|
+
stroke: var(--mk-paint);
|
|
83
|
+
stroke-width: 1.6;
|
|
84
|
+
stroke-linejoin: round;
|
|
85
|
+
}
|
|
86
|
+
`;
|
|
87
|
+
}
|
|
88
|
+
function markCss() {
|
|
89
|
+
return `
|
|
90
|
+
.mk-marks {
|
|
91
|
+
position: absolute;
|
|
92
|
+
inset: 0;
|
|
93
|
+
pointer-events: none;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
.mk-mark {
|
|
97
|
+
${POSITIONED}
|
|
98
|
+
--mk-mark-up: 1.18;
|
|
99
|
+
display: grid;
|
|
100
|
+
place-items: center;
|
|
101
|
+
width: 38px;
|
|
102
|
+
height: 38px;
|
|
103
|
+
padding: 0;
|
|
104
|
+
border: 0;
|
|
105
|
+
border-radius: var(--mk-r-xs);
|
|
106
|
+
background: transparent;
|
|
107
|
+
color: var(--mk-ink);
|
|
108
|
+
cursor: pointer;
|
|
109
|
+
pointer-events: auto;
|
|
110
|
+
filter: drop-shadow(var(--mk-sh1));
|
|
111
|
+
animation: mk-mark-in var(--mk-dur-mark-in) var(--mk-ease-entrance);
|
|
112
|
+
transition:
|
|
113
|
+
transform var(--mk-dur-fade) var(--mk-ease-surface),
|
|
114
|
+
opacity var(--mk-dur-fade) var(--mk-ease-surface);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/* A row pointed at is the same gesture as a mark pointed at, so it gets the
|
|
118
|
+
same answer: the mark grows and comes forward. The glow below is the click's
|
|
119
|
+
alone — that one outlives the hand, and this does not. */
|
|
120
|
+
.mk-mark:hover,
|
|
121
|
+
.mk-mark:focus-visible,
|
|
122
|
+
.mk-mark[data-mk-peeked="true"],
|
|
123
|
+
.mk-mark[aria-pressed="true"] {
|
|
124
|
+
transform: scale(var(--mk-mark-up));
|
|
125
|
+
opacity: 1;
|
|
126
|
+
z-index: 3;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
.mk-mark:active {
|
|
130
|
+
transform: scale(calc(var(--mk-mark-up) * var(--mk-press)));
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/* Moved out of the way of what it was covering. The cursor says so before the
|
|
134
|
+
drag starts, because nothing else on the page suggests a mark can move. */
|
|
135
|
+
.mk-mark[data-mk-nudged="true"] {
|
|
136
|
+
cursor: grab;
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
.mk-mark[data-mk-dragging="true"] {
|
|
140
|
+
cursor: grabbing;
|
|
141
|
+
transition: none;
|
|
142
|
+
z-index: 4;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/* A box-shadow on the 38px button draws a rounded square behind a leaf, which
|
|
146
|
+
reads as a second object. A drop-shadow takes the alpha, so it takes the leaf. */
|
|
147
|
+
.mk-mark[aria-pressed="true"] {
|
|
148
|
+
filter:
|
|
149
|
+
drop-shadow(0 0 3px color-mix(in oklab, var(--mk-paint) 70%, transparent))
|
|
150
|
+
drop-shadow(0 0 9px color-mix(in oklab, var(--mk-paint) 45%, transparent))
|
|
151
|
+
drop-shadow(var(--mk-sh1));
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
.mk-mark[aria-pressed="true"] .mk-leaf-halo {
|
|
155
|
+
stroke-width: 10;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/* The leaf's mass sits below its middle — the stem is the long end — so a
|
|
159
|
+
box-centred number reads high inside it. 1px down is where it looks centred,
|
|
160
|
+
which is 2px from where the box says it is. */
|
|
161
|
+
.mk-mark-n {
|
|
162
|
+
--mk-n: 11px;
|
|
163
|
+
position: relative;
|
|
164
|
+
font-size: var(--mk-n);
|
|
165
|
+
font-weight: 800;
|
|
166
|
+
line-height: 1;
|
|
167
|
+
letter-spacing: -0.02em;
|
|
168
|
+
color: var(--mk-ink);
|
|
169
|
+
transform: translateY(1px);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/* Two digits are twice the width in the same waist. The leaf does not grow —
|
|
173
|
+
a mark that changed size at the tenth comment would be a different object —
|
|
174
|
+
so the number gives way instead. */
|
|
175
|
+
.mk-mark-n[data-mk-digits="2"] {
|
|
176
|
+
font-size: calc(var(--mk-n) * 0.9);
|
|
177
|
+
letter-spacing: -0.05em;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
.mk-mark-n[data-mk-digits="3"] {
|
|
181
|
+
font-size: calc(var(--mk-n) * 0.78);
|
|
182
|
+
letter-spacing: -0.06em;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
:is(.mk-mark, .mk-rowleaf)[data-form="outline"] .mk-mark-n {
|
|
186
|
+
color: var(--mk-paint);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/* A half-filled leaf puts the waterline through the number, so the glyph is
|
|
190
|
+
two colours at once and legible in neither. Stroking it in the paint, under
|
|
191
|
+
the fill, sits it on its own colour wherever the waterline happens to fall. */
|
|
192
|
+
:is(.mk-mark, .mk-rowleaf)[data-form="partial"] .mk-mark-n {
|
|
193
|
+
paint-order: stroke fill;
|
|
194
|
+
-webkit-text-stroke: 2.5px var(--mk-paint);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/* A weak anchor thins the fill and leaves the ring at full strength: how sure
|
|
198
|
+
the anchor is and how far through its life the comment is are two signals,
|
|
199
|
+
and one washing out the other is how they stop being two. */
|
|
200
|
+
.mk-mark[data-confidence="weak"] .mk-leaf-body {
|
|
201
|
+
fill-opacity: 0.42;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
.mk-mark[data-confidence="weak"] .mk-leaf-edge {
|
|
205
|
+
fill-opacity: 1;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
@keyframes mk-mark-in {
|
|
209
|
+
from {
|
|
210
|
+
opacity: 0;
|
|
211
|
+
transform: translateY(var(--mk-rise-mark));
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
`;
|
|
215
|
+
}
|
|
216
|
+
function ringCss() {
|
|
217
|
+
return `
|
|
218
|
+
.mk-ring {
|
|
219
|
+
${POSITIONED}
|
|
220
|
+
width: var(--mk-w);
|
|
221
|
+
height: var(--mk-h);
|
|
222
|
+
border-radius: var(--mk-r-sm);
|
|
223
|
+
pointer-events: none;
|
|
224
|
+
box-shadow:
|
|
225
|
+
0 0 0 2px var(--mk-accent),
|
|
226
|
+
0 0 0 7px color-mix(in oklab, var(--mk-accent) 20%, transparent);
|
|
227
|
+
animation: mk-ring-in var(--mk-dur-fade) var(--mk-ease-surface);
|
|
228
|
+
transition: opacity var(--mk-dur-fade) var(--mk-ease-surface);
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
.mk-ring[data-mk-moving] {
|
|
232
|
+
will-change: transform;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
.mk-ring[data-mk-state="hovered"] {
|
|
236
|
+
box-shadow: 0 0 0 1.5px color-mix(in oklab, var(--mk-accent) 60%, transparent);
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
/* A passage is its own lines and nothing else: the box around them is the
|
|
240
|
+
paragraph, and outlining that says the comment is on the paragraph. */
|
|
241
|
+
.mk-ring[data-mk-passage="true"] {
|
|
242
|
+
box-shadow: none;
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/* A region is an area, not a thing: it is filled as well as outlined, in the
|
|
246
|
+
picker's own band, so what a reviewer drew is what they get back. It has no
|
|
247
|
+
outer halo — the halo reads as a margin around an element, and there is no
|
|
248
|
+
element here to have one. */
|
|
249
|
+
.mk-ring[data-mk-region="true"] {
|
|
250
|
+
border-radius: var(--mk-r-xs);
|
|
251
|
+
background: color-mix(in oklab, var(--mk-accent) 18%, transparent);
|
|
252
|
+
box-shadow: 0 0 0 2px var(--mk-accent);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
|
|
256
|
+
/* Anchored to the ring's edge rather than offset by a number: the label is two
|
|
257
|
+
lines in developer detail and one line the rest of the time. */
|
|
258
|
+
.mk-ring-label {
|
|
259
|
+
position: absolute;
|
|
260
|
+
bottom: 100%;
|
|
261
|
+
left: -2px;
|
|
262
|
+
display: flex;
|
|
263
|
+
flex-direction: column;
|
|
264
|
+
max-width: 260px;
|
|
265
|
+
margin-bottom: 3px;
|
|
266
|
+
padding: 1px 7px 2px;
|
|
267
|
+
border-radius: var(--mk-r-xs) var(--mk-r-xs) var(--mk-r-xs) 0;
|
|
268
|
+
background: var(--mk-accent);
|
|
269
|
+
color: var(--mk-accent-ink);
|
|
270
|
+
font-size: 10px;
|
|
271
|
+
font-weight: 600;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
.mk-ring-label[data-mk-below] {
|
|
275
|
+
bottom: auto;
|
|
276
|
+
top: 100%;
|
|
277
|
+
margin: 3px 0 0;
|
|
278
|
+
border-radius: 0 var(--mk-r-xs) var(--mk-r-xs) var(--mk-r-xs);
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
.mk-ring-name,
|
|
282
|
+
.mk-ring-note {
|
|
283
|
+
display: block;
|
|
284
|
+
white-space: nowrap;
|
|
285
|
+
overflow: hidden;
|
|
286
|
+
text-overflow: ellipsis;
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/* Quieter than the name: it is where to go next, not what this is. */
|
|
290
|
+
.mk-ring-note {
|
|
291
|
+
font-size: 9px;
|
|
292
|
+
font-weight: 500;
|
|
293
|
+
color: color-mix(in oklab, var(--mk-accent-ink) 78%, transparent);
|
|
294
|
+
}
|
|
295
|
+
|
|
296
|
+
/* The wash a reviewer reads as "this text": the same green the ring is drawn
|
|
297
|
+
in, at the weight a selection has, so the passage is what is highlighted
|
|
298
|
+
and the element around it is not. */
|
|
299
|
+
.mk-ring-run {
|
|
300
|
+
${POSITIONED}
|
|
301
|
+
width: var(--mk-w);
|
|
302
|
+
height: var(--mk-h);
|
|
303
|
+
border-radius: 3px;
|
|
304
|
+
background: color-mix(in oklab, var(--mk-accent) 32%, transparent);
|
|
305
|
+
box-shadow: 0 0 0 1px color-mix(in oklab, var(--mk-accent) 45%, transparent);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
@keyframes mk-ring-in {
|
|
309
|
+
from {
|
|
310
|
+
opacity: 0;
|
|
311
|
+
}
|
|
312
|
+
}
|
|
313
|
+
`;
|
|
314
|
+
}
|
|
315
|
+
function avatarCss() {
|
|
316
|
+
return `
|
|
317
|
+
.mk-avatar {
|
|
318
|
+
position: relative;
|
|
319
|
+
display: grid;
|
|
320
|
+
place-items: center;
|
|
321
|
+
width: 22px;
|
|
322
|
+
height: 22px;
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
.mk-avatar .mk-leaf-body {
|
|
326
|
+
stroke-width: 2;
|
|
327
|
+
}
|
|
328
|
+
|
|
329
|
+
.mk-avatar-ini {
|
|
330
|
+
position: relative;
|
|
331
|
+
font-size: 8px;
|
|
332
|
+
font-weight: 800;
|
|
333
|
+
letter-spacing: -0.02em;
|
|
334
|
+
color: var(--mk-ink);
|
|
335
|
+
transform: translateY(0.5px);
|
|
336
|
+
}
|
|
337
|
+
|
|
338
|
+
.mk-avatar[data-provenance="client"] .mk-leaf-body {
|
|
339
|
+
fill-opacity: 0.4;
|
|
340
|
+
stroke-width: 2.5;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
.mk-avatar[data-provenance="client"] .mk-avatar-ini {
|
|
344
|
+
color: var(--mk-fg);
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
.mk-avatar[data-provenance="guest"] .mk-avatar-ini {
|
|
348
|
+
color: var(--mk-paint);
|
|
349
|
+
}
|
|
350
|
+
`;
|
|
351
|
+
}
|
|
352
|
+
function cullCss() {
|
|
353
|
+
return `
|
|
354
|
+
.mk-mark[data-mk-off],
|
|
355
|
+
.mk-ring[data-mk-off] {
|
|
356
|
+
visibility: hidden;
|
|
357
|
+
}
|
|
358
|
+
`;
|
|
359
|
+
}
|
|
360
|
+
function marksCss() {
|
|
361
|
+
return [
|
|
362
|
+
paintCss(),
|
|
363
|
+
leafCss(),
|
|
364
|
+
markCss(),
|
|
365
|
+
ringCss(),
|
|
366
|
+
avatarCss(),
|
|
367
|
+
cullCss()
|
|
368
|
+
].map((block) => block.trim()).join("\n\n");
|
|
369
|
+
}
|
|
370
|
+
//#endregion
|
|
371
|
+
export { marksCss };
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
//#region src/marks/frame.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* The scroll loop: one animation frame per scrolled frame, for everything.
|
|
4
|
+
*
|
|
5
|
+
* Marks and the ring are absolutely positioned over a page that moves under
|
|
6
|
+
* them, so they are measured and moved per frame rather than transitioned —
|
|
7
|
+
* a transition on a position lags a frame behind the page and reads as broken.
|
|
8
|
+
* One loop repaints all of them, because two loops disagree on a slow frame.
|
|
9
|
+
*/
|
|
10
|
+
/** What a loop repaints, told whether the page is currently moving under it. */
|
|
11
|
+
export type Paint = (moving: boolean) => void;
|
|
12
|
+
/**
|
|
13
|
+
* Repaints on scroll and resize, coalesced into one frame. `scrollend` is what
|
|
14
|
+
* drops `will-change` again: a compositor layer held forever costs memory on a
|
|
15
|
+
* page Maple is only a guest on.
|
|
16
|
+
*/
|
|
17
|
+
export declare function startFrameLoop(view: Window, paint: Paint): () => void;
|
|
18
|
+
/**
|
|
19
|
+
* Runs a loop for as long as a part is mounted, and repaints after every
|
|
20
|
+
* render, so nothing is ever seen at the position React first rendered it at.
|
|
21
|
+
*/
|
|
22
|
+
export declare function useFrameLoop(part: string, paint: Paint): void;
|
|
23
|
+
/** The viewport height a cull is measured against. */
|
|
24
|
+
export declare function viewportHeight(container: Element): number;
|
|
25
|
+
//#endregion
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { useMapleUi } from "../context.js";
|
|
2
|
+
import { useLayoutEffect, useRef } from "react";
|
|
3
|
+
//#region src/marks/frame.ts
|
|
4
|
+
const PASSIVE = {
|
|
5
|
+
capture: true,
|
|
6
|
+
passive: true
|
|
7
|
+
};
|
|
8
|
+
function startFrameLoop(view, paint) {
|
|
9
|
+
let frame = 0;
|
|
10
|
+
let moving = false;
|
|
11
|
+
const run = () => {
|
|
12
|
+
frame = 0;
|
|
13
|
+
paint(moving);
|
|
14
|
+
};
|
|
15
|
+
const schedule = () => {
|
|
16
|
+
if (frame === 0) frame = view.requestAnimationFrame(run);
|
|
17
|
+
};
|
|
18
|
+
const onScroll = () => {
|
|
19
|
+
moving = true;
|
|
20
|
+
schedule();
|
|
21
|
+
};
|
|
22
|
+
const onSettle = () => {
|
|
23
|
+
moving = false;
|
|
24
|
+
schedule();
|
|
25
|
+
};
|
|
26
|
+
view.addEventListener("scroll", onScroll, PASSIVE);
|
|
27
|
+
view.addEventListener("scrollend", onSettle, PASSIVE);
|
|
28
|
+
view.addEventListener("resize", schedule, PASSIVE);
|
|
29
|
+
return () => {
|
|
30
|
+
view.removeEventListener("scroll", onScroll, PASSIVE);
|
|
31
|
+
view.removeEventListener("scrollend", onSettle, PASSIVE);
|
|
32
|
+
view.removeEventListener("resize", schedule, PASSIVE);
|
|
33
|
+
if (frame !== 0) view.cancelAnimationFrame(frame);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
function useFrameLoop(part, paint) {
|
|
37
|
+
const { container } = useMapleUi(part);
|
|
38
|
+
const latest = useRef(paint);
|
|
39
|
+
useLayoutEffect(() => {
|
|
40
|
+
latest.current = paint;
|
|
41
|
+
paint(false);
|
|
42
|
+
});
|
|
43
|
+
useLayoutEffect(() => {
|
|
44
|
+
const view = container.ownerDocument.defaultView;
|
|
45
|
+
if (!view) return;
|
|
46
|
+
return startFrameLoop(view, (moving) => latest.current(moving));
|
|
47
|
+
}, [container]);
|
|
48
|
+
}
|
|
49
|
+
function viewportHeight(container) {
|
|
50
|
+
return container.ownerDocument.defaultView?.innerHeight ?? 0;
|
|
51
|
+
}
|
|
52
|
+
//#endregion
|
|
53
|
+
export { startFrameLoop, useFrameLoop, viewportHeight };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
//#region src/marks/geometry.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Where a mark goes and how big its half-fill is.
|
|
4
|
+
*
|
|
5
|
+
* The collision resolver steps a colliding mark sideways at most three times:
|
|
6
|
+
* a fourth is further from its anchor than it is worth. Hit areas are 40px and
|
|
7
|
+
* the step is wider than the visible mark, so two marks' hit areas never
|
|
8
|
+
* overlap — which is half of what the resolver exists for.
|
|
9
|
+
*/
|
|
10
|
+
/** The visible mark. The hit area is larger and extended with a pseudo-element. */
|
|
11
|
+
export declare const MARK_SIZE_PX = 38;
|
|
12
|
+
/** The smallest a control may be to be hit reliably with a thumb. */
|
|
13
|
+
export declare const MARK_HIT_PX = 40;
|
|
14
|
+
/** A mark sits just outside its anchor's top-left corner. */
|
|
15
|
+
export declare const MARK_OFFSET_PX = 16;
|
|
16
|
+
/** Two candidates collide when both axes are inside this: one whole hit area. */
|
|
17
|
+
export declare const COLLISION_GAP_PX = 40;
|
|
18
|
+
/** How far a colliding mark steps sideways before trying again. */
|
|
19
|
+
export declare const COLLISION_STEP_PX = 42;
|
|
20
|
+
/** Three tries. A fourth lands further from the anchor than it is worth. */
|
|
21
|
+
export declare const COLLISION_MAX_TRIES = 3;
|
|
22
|
+
/** The clip rectangle that gives the half-filled form its horizontal waterline. */
|
|
23
|
+
export interface Waterline {
|
|
24
|
+
readonly x: number;
|
|
25
|
+
readonly y: number;
|
|
26
|
+
readonly width: number;
|
|
27
|
+
readonly height: number;
|
|
28
|
+
}
|
|
29
|
+
/**
|
|
30
|
+
* The clip sits outside the rotation, so the waterline stays horizontal on
|
|
31
|
+
* screen while the leaf stays tilted. A fraction outside 0..1 is clamped.
|
|
32
|
+
*/
|
|
33
|
+
export declare function waterline(fraction: number): Waterline;
|
|
34
|
+
/** A box in viewport coordinates, which is what the overlay's layer is in. */
|
|
35
|
+
export interface Box {
|
|
36
|
+
readonly x: number;
|
|
37
|
+
readonly y: number;
|
|
38
|
+
readonly width: number;
|
|
39
|
+
readonly height: number;
|
|
40
|
+
}
|
|
41
|
+
/** The overlay is `position: fixed`, so a client rect is already its geometry. */
|
|
42
|
+
export declare function ringBox(rect: Box): Box;
|
|
43
|
+
/** One quote run, placed inside the ring rather than against the viewport. */
|
|
44
|
+
export declare function runBox(run: Box, rect: Box): Box;
|
|
45
|
+
/** Far enough above or below the viewport that drawing it is wasted work. */
|
|
46
|
+
export declare function culled(rect: Box, viewportHeight: number): boolean;
|
|
47
|
+
/** Where a mark wants to be: just outside its anchor's top-left corner. */
|
|
48
|
+
export declare function markSpot(rect: Box): Box;
|
|
49
|
+
/**
|
|
50
|
+
* Steps a colliding mark sideways until it clears every mark already placed.
|
|
51
|
+
* It gives up after three: a fourth is further from its anchor than it is
|
|
52
|
+
* worth, and a mark that has wandered names the wrong thing.
|
|
53
|
+
*/
|
|
54
|
+
export declare function placeMark(wanted: Box, taken: readonly Box[]): Box;
|
|
55
|
+
//#endregion
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
//#region src/marks/geometry.ts
|
|
2
|
+
const MARK_SIZE_PX = 38;
|
|
3
|
+
const MARK_HIT_PX = 40;
|
|
4
|
+
const MARK_OFFSET_PX = 16;
|
|
5
|
+
const COLLISION_GAP_PX = 40;
|
|
6
|
+
const COLLISION_STEP_PX = 42;
|
|
7
|
+
const COLLISION_MAX_TRIES = 3;
|
|
8
|
+
const BOX = 78;
|
|
9
|
+
const BOTTOM = 71;
|
|
10
|
+
const ORIGIN = -7;
|
|
11
|
+
function waterline(fraction) {
|
|
12
|
+
const height = BOX * Math.min(Math.max(fraction, 0), 1);
|
|
13
|
+
return {
|
|
14
|
+
x: ORIGIN,
|
|
15
|
+
y: BOTTOM - height,
|
|
16
|
+
width: BOX,
|
|
17
|
+
height
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
const RING_INSET_PX = 3;
|
|
21
|
+
const CULL_PX = 60;
|
|
22
|
+
function ringBox(rect) {
|
|
23
|
+
return {
|
|
24
|
+
x: Math.round(rect.x - RING_INSET_PX),
|
|
25
|
+
y: Math.round(rect.y - RING_INSET_PX),
|
|
26
|
+
width: Math.round(rect.width + 6),
|
|
27
|
+
height: Math.round(rect.height + 6)
|
|
28
|
+
};
|
|
29
|
+
}
|
|
30
|
+
function runBox(run, rect) {
|
|
31
|
+
return {
|
|
32
|
+
x: Math.round(run.x - rect.x + RING_INSET_PX),
|
|
33
|
+
y: Math.round(run.y - rect.y + RING_INSET_PX),
|
|
34
|
+
width: Math.round(run.width),
|
|
35
|
+
height: Math.round(run.height)
|
|
36
|
+
};
|
|
37
|
+
}
|
|
38
|
+
function culled(rect, viewportHeight) {
|
|
39
|
+
return rect.y + rect.height < -60 || rect.y > viewportHeight + CULL_PX;
|
|
40
|
+
}
|
|
41
|
+
function markSpot(rect) {
|
|
42
|
+
return {
|
|
43
|
+
x: Math.max(2, Math.round(rect.x - 16)),
|
|
44
|
+
y: Math.max(2, Math.round(rect.y - 16)),
|
|
45
|
+
width: 38,
|
|
46
|
+
height: 38
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
function collides(spot, taken) {
|
|
50
|
+
return taken.some((other) => Math.abs(other.x - spot.x) < 40 && Math.abs(other.y - spot.y) < 40);
|
|
51
|
+
}
|
|
52
|
+
function placeMark(wanted, taken) {
|
|
53
|
+
let spot = wanted;
|
|
54
|
+
for (let tries = 0; tries < 3 && collides(spot, taken); tries += 1) spot = {
|
|
55
|
+
...spot,
|
|
56
|
+
x: spot.x + 42
|
|
57
|
+
};
|
|
58
|
+
return spot;
|
|
59
|
+
}
|
|
60
|
+
//#endregion
|
|
61
|
+
export { COLLISION_GAP_PX, COLLISION_MAX_TRIES, COLLISION_STEP_PX, MARK_HIT_PX, MARK_OFFSET_PX, MARK_SIZE_PX, culled, markSpot, placeMark, ringBox, runBox, waterline };
|