@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.
Files changed (173) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +39 -0
  4. package/dist/composer/actions.d.ts +15 -0
  5. package/dist/composer/actions.js +59 -0
  6. package/dist/composer/attachments.d.ts +33 -0
  7. package/dist/composer/attachments.js +166 -0
  8. package/dist/composer/badge.d.ts +13 -0
  9. package/dist/composer/badge.js +25 -0
  10. package/dist/composer/body.d.ts +13 -0
  11. package/dist/composer/body.js +119 -0
  12. package/dist/composer/composer.d.ts +23 -0
  13. package/dist/composer/composer.js +122 -0
  14. package/dist/composer/css.js +559 -0
  15. package/dist/composer/detail.d.ts +32 -0
  16. package/dist/composer/detail.js +102 -0
  17. package/dist/composer/emoji.d.ts +54 -0
  18. package/dist/composer/emoji.js +131 -0
  19. package/dist/composer/index.d.ts +15 -0
  20. package/dist/composer/index.js +15 -0
  21. package/dist/composer/inset.js +20 -0
  22. package/dist/composer/leave.d.ts +19 -0
  23. package/dist/composer/leave.js +34 -0
  24. package/dist/composer/peek.d.ts +21 -0
  25. package/dist/composer/peek.js +29 -0
  26. package/dist/composer/phrase.d.ts +27 -0
  27. package/dist/composer/phrase.js +30 -0
  28. package/dist/composer/picker-emoji.d.ts +34 -0
  29. package/dist/composer/picker-emoji.js +48 -0
  30. package/dist/composer/prompt.js +31 -0
  31. package/dist/composer/scope.d.ts +11 -0
  32. package/dist/composer/scope.js +16 -0
  33. package/dist/composer/target.d.ts +13 -0
  34. package/dist/composer/target.js +45 -0
  35. package/dist/context.d.ts +21 -0
  36. package/dist/context.js +16 -0
  37. package/dist/data.d.ts +52 -0
  38. package/dist/data.js +30 -0
  39. package/dist/icons/branch.d.ts +4 -0
  40. package/dist/icons/branch.js +8 -0
  41. package/dist/icons/cog.d.ts +4 -0
  42. package/dist/icons/cog.js +8 -0
  43. package/dist/icons/commit.d.ts +4 -0
  44. package/dist/icons/commit.js +5 -0
  45. package/dist/icons/crossfade.d.ts +15 -0
  46. package/dist/icons/crossfade.js +27 -0
  47. package/dist/icons/element.d.ts +4 -0
  48. package/dist/icons/element.js +5 -0
  49. package/dist/icons/icon.d.ts +24 -0
  50. package/dist/icons/icon.js +27 -0
  51. package/dist/icons/index.d.ts +13 -0
  52. package/dist/icons/index.js +13 -0
  53. package/dist/icons/noplace.d.ts +4 -0
  54. package/dist/icons/noplace.js +5 -0
  55. package/dist/icons/region.d.ts +4 -0
  56. package/dist/icons/region.js +8 -0
  57. package/dist/icons/smiley.d.ts +4 -0
  58. package/dist/icons/smiley.js +8 -0
  59. package/dist/icons/sparkle.d.ts +4 -0
  60. package/dist/icons/sparkle.js +8 -0
  61. package/dist/icons/target.d.ts +4 -0
  62. package/dist/icons/target.js +5 -0
  63. package/dist/icons/text.d.ts +4 -0
  64. package/dist/icons/text.js +5 -0
  65. package/dist/index.d.ts +11 -0
  66. package/dist/index.js +11 -0
  67. package/dist/island/account.d.ts +10 -0
  68. package/dist/island/account.js +52 -0
  69. package/dist/island/comments.d.ts +24 -0
  70. package/dist/island/comments.js +40 -0
  71. package/dist/island/content.d.ts +10 -0
  72. package/dist/island/content.js +26 -0
  73. package/dist/island/context.d.ts +58 -0
  74. package/dist/island/context.js +23 -0
  75. package/dist/island/css.d.ts +13 -0
  76. package/dist/island/css.js +1020 -0
  77. package/dist/island/drag.d.ts +10 -0
  78. package/dist/island/drag.js +77 -0
  79. package/dist/island/filters.d.ts +10 -0
  80. package/dist/island/filters.js +63 -0
  81. package/dist/island/header.d.ts +32 -0
  82. package/dist/island/header.js +47 -0
  83. package/dist/island/index.d.ts +18 -0
  84. package/dist/island/index.js +18 -0
  85. package/dist/island/island.d.ts +12 -0
  86. package/dist/island/island.js +111 -0
  87. package/dist/island/item.d.ts +12 -0
  88. package/dist/island/item.js +158 -0
  89. package/dist/island/language.d.ts +55 -0
  90. package/dist/island/language.js +125 -0
  91. package/dist/island/leaf.js +17 -0
  92. package/dist/island/list.d.ts +12 -0
  93. package/dist/island/list.js +31 -0
  94. package/dist/island/new-comment.d.ts +18 -0
  95. package/dist/island/new-comment.js +56 -0
  96. package/dist/island/settings.d.ts +10 -0
  97. package/dist/island/settings.js +134 -0
  98. package/dist/island/stagger.d.ts +12 -0
  99. package/dist/island/stagger.js +4 -0
  100. package/dist/island/time.d.ts +17 -0
  101. package/dist/island/time.js +29 -0
  102. package/dist/island/trigger.d.ts +10 -0
  103. package/dist/island/trigger.js +39 -0
  104. package/dist/language.d.ts +45 -0
  105. package/dist/language.js +66 -0
  106. package/dist/maple.d.ts +25 -0
  107. package/dist/maple.js +65 -0
  108. package/dist/marks/avatar.d.ts +16 -0
  109. package/dist/marks/avatar.js +36 -0
  110. package/dist/marks/css.d.ts +12 -0
  111. package/dist/marks/css.js +371 -0
  112. package/dist/marks/frame.d.ts +25 -0
  113. package/dist/marks/frame.js +53 -0
  114. package/dist/marks/geometry.d.ts +55 -0
  115. package/dist/marks/geometry.js +61 -0
  116. package/dist/marks/index.d.ts +14 -0
  117. package/dist/marks/index.js +14 -0
  118. package/dist/marks/label.d.ts +23 -0
  119. package/dist/marks/label.js +31 -0
  120. package/dist/marks/layer.d.ts +16 -0
  121. package/dist/marks/layer.js +194 -0
  122. package/dist/marks/leaf.d.ts +18 -0
  123. package/dist/marks/leaf.js +7 -0
  124. package/dist/marks/mark.d.ts +28 -0
  125. package/dist/marks/mark.js +43 -0
  126. package/dist/marks/nudge.d.ts +50 -0
  127. package/dist/marks/nudge.js +76 -0
  128. package/dist/marks/paint.d.ts +11 -0
  129. package/dist/marks/paint.js +19 -0
  130. package/dist/marks/placement.d.ts +24 -0
  131. package/dist/marks/placement.js +28 -0
  132. package/dist/marks/ring.d.ts +28 -0
  133. package/dist/marks/ring.js +85 -0
  134. package/dist/marks/shape.d.ts +21 -0
  135. package/dist/marks/shape.js +51 -0
  136. package/dist/notice/css.d.ts +11 -0
  137. package/dist/notice/css.js +74 -0
  138. package/dist/notice/index.d.ts +4 -0
  139. package/dist/notice/index.js +4 -0
  140. package/dist/notice/language.d.ts +15 -0
  141. package/dist/notice/language.js +14 -0
  142. package/dist/notice/notice.d.ts +14 -0
  143. package/dist/notice/notice.js +49 -0
  144. package/dist/part.d.ts +9 -0
  145. package/dist/part.js +11 -0
  146. package/dist/picker/css.d.ts +13 -0
  147. package/dist/picker/css.js +141 -0
  148. package/dist/picker/index.d.ts +6 -0
  149. package/dist/picker/index.js +6 -0
  150. package/dist/picker/language.d.ts +14 -0
  151. package/dist/picker/language.js +17 -0
  152. package/dist/picker/picker.d.ts +18 -0
  153. package/dist/picker/picker.js +164 -0
  154. package/dist/picker/session.d.ts +16 -0
  155. package/dist/picker/session.js +35 -0
  156. package/dist/picker/target.d.ts +23 -0
  157. package/dist/picker/target.js +37 -0
  158. package/dist/root.d.ts +42 -0
  159. package/dist/root.js +75 -0
  160. package/dist/shots.js +32 -0
  161. package/dist/slot.d.ts +28 -0
  162. package/dist/slot.js +52 -0
  163. package/dist/slots.d.ts +24 -0
  164. package/dist/slots.js +75 -0
  165. package/dist/stylesheet.d.ts +15 -0
  166. package/dist/stylesheet.js +188 -0
  167. package/dist/theme.d.ts +20 -0
  168. package/dist/theme.js +13 -0
  169. package/dist/tip.d.ts +40 -0
  170. package/dist/tip.js +82 -0
  171. package/dist/tokens.d.ts +28 -0
  172. package/dist/tokens.js +180 -0
  173. 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 };