@maple-kit/ui 0.11.0 → 0.12.1

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/README.md CHANGED
@@ -5,6 +5,13 @@ composer. The surface a person actually touches on a preview deployment.
5
5
 
6
6
  **Pre-release.** Every package here is 0.x and makes no compatibility promise.
7
7
 
8
+ <table>
9
+ <tr>
10
+ <td width="50%" valign="top"><img src="https://raw.githubusercontent.com/maple-kit/maple/main/docs/assets/features/three-types.gif" alt="Three picks in a row: a metric card, a box dragged over part of a chart, and a sentence selected under it." width="100%"><br><b>An element, an area, or a passage.</b> Pick a component, drag a box over part of the page, or select the words that are wrong.</td>
11
+ <td width="50%" valign="top"><img src="https://raw.githubusercontent.com/maple-kit/maple/main/docs/assets/features/overlay.gif" alt="A reviewer clicks a comment on a metric card, and the sheet opens with the comment, its author, its status, and the width and theme it was written at." width="100%"><br><b>Every comment carries its context.</b> The width, the colour scheme, a screenshot taken at the pick, and who wrote it.</td>
12
+ </tr>
13
+ </table>
14
+
8
15
  ## Install
9
16
 
10
17
  ```sh
@@ -14,14 +21,37 @@ npm install @maple-kit/ui
14
21
  `react` and `react-dom` are peer dependencies. `@maple-kit/core` and
15
22
  `@maple-kit/react` come with it.
16
23
 
24
+ ## Use
25
+
26
+ ```tsx
27
+ import { Maple } from "@maple-kit/ui/maple";
28
+
29
+ <Maple branch={import.meta.env.VITE_MAPLE_BRANCH} />;
30
+ ```
31
+
32
+ It talks to the route [`@maple-kit/core`](../core) mounts at `/api/maple`.
33
+ Render it on preview builds only; `?maple=off` turns it off without a rebuild.
34
+
17
35
  ## What is in it
18
36
 
19
37
  `@maple-kit/ui/maple` is the whole overlay as one component. The parts are also
20
- exported on their own: `/marks`, `/island`, `/composer`, `/picker`, `/notice`,
21
- `/mock` and `/icons`.
38
+ exported on their own, for a host that wants to compose them differently:
39
+
40
+ | Entry | What it is |
41
+ | ------------------------ | ------------------------------------------------------------------- |
42
+ | `@maple-kit/ui/maple` | `<Maple />`: everything below, composed. |
43
+ | `@maple-kit/ui/marks` | The leaf on each commented element, and the ring around its target. |
44
+ | `@maple-kit/ui/island` | The corner pill that opens into the list of comments. |
45
+ | `@maple-kit/ui/composer` | The sheet a comment is written in, with its screenshot and score. |
46
+ | `@maple-kit/ui/picker` | Element, text and region picks. |
47
+ | `@maple-kit/ui/notice` | Failures the overlay reports in its own voice. |
48
+ | `@maple-kit/ui/mock` | `<MapleMock />`, Maple Mock's box, for a page that mocks alone. |
49
+ | `@maple-kit/ui/icons` | The thirteen icons, one module each. |
22
50
 
23
- `@maple-kit/ui/mock` is Maple Mock's box, `<MapleMock />`, for a page that
24
- mocks without the review overlay. Inside `<Maple />` it is already mounted.
51
+ Inside `<Maple />` the mock box is already mounted. [`@maple-kit/mock`](../mock)
52
+ shows what it does.
53
+
54
+ ## Mounting under a strict CSP
25
55
 
26
56
  Everything renders inside one shadow root, styled only through
27
57
  `new CSSStyleSheet()` and `adoptedStyleSheets`, with positions set by
@@ -32,9 +62,15 @@ it stays checkable by reading one function.
32
62
  Every part carries a size budget, held in CI by `scripts/size.js`. Raising one
33
63
  is a decision recorded in a commit, not a number that drifts.
34
64
 
65
+ An application that would rather draw review comments in its own design
66
+ system uses the hooks in [`@maple-kit/react`](../react) and none of this
67
+ package.
68
+
35
69
  ## Documentation
36
70
 
37
71
  - [The overlay and CSP](https://github.com/maple-kit/maple/blob/main/docs/overlay-csp.md)
72
+ - [Drafts and publishing](https://github.com/maple-kit/maple/blob/main/docs/drafts.md)
73
+ - [Screenshots](https://github.com/maple-kit/maple/blob/main/docs/screenshots.md)
38
74
  - [UI conventions](https://github.com/maple-kit/maple/blob/main/docs/ui-conventions.md)
39
75
 
40
76
  ## Licence
@@ -7,7 +7,7 @@ export interface MapleActionsProps extends AsChildProps {
7
7
  readonly children?: ReactNode;
8
8
  }
9
9
  /** Closes the composer and keeps what was written, unsent. */
10
- export declare const KEEP_LABEL = "Keep";
10
+ export declare const SAVE_DRAFT_LABEL = "Save as draft";
11
11
  /** The verb, because publishing is the act and the comment already exists. */
12
12
  export declare const PUBLISH_LABEL = "Publish";
13
13
  /** The two controls, and room beside them for a later one. */
@@ -3,7 +3,7 @@ import { useComposerScope } from "./scope.js";
3
3
  import { createElement, forwardRef } from "react";
4
4
  import { useMaple, useMapleClient } from "@maple-kit/react";
5
5
  //#region src/composer/actions.ts
6
- const KEEP_LABEL = "Keep";
6
+ const SAVE_DRAFT_LABEL = "Save as draft";
7
7
  const PUBLISH_LABEL = "Publish";
8
8
  const VIEW_LABELS = {
9
9
  close: "Close",
@@ -13,11 +13,8 @@ const VIEW_LABELS = {
13
13
  const MapleActions = forwardRef(function MapleActions(props, ref) {
14
14
  const { composer, publishing } = useMaple();
15
15
  const client = useMapleClient();
16
- const scope = useComposerScope("Maple.Actions");
17
16
  const Element = props.asChild ? Slot : "footer";
18
- const publish = () => {
19
- client.publish().then(() => scope.clear(), () => void 0);
20
- };
17
+ const publish = usePublish();
21
18
  const className = props.className ? `mk-composer-foot ${props.className}` : "mk-composer-foot";
22
19
  if (composer.viewing !== void 0) return createElement(Element, {
23
20
  ref,
@@ -31,13 +28,20 @@ const MapleActions = forwardRef(function MapleActions(props, ref) {
31
28
  className: "mk-btn mk-btn-quiet mk-press",
32
29
  disabled: composer.body.trim() === "",
33
30
  onClick: () => client.keepDraft()
34
- }, KEEP_LABEL), createElement("button", {
31
+ }, SAVE_DRAFT_LABEL), createElement("button", {
35
32
  type: "button",
36
33
  className: "mk-btn mk-btn-primary mk-press",
37
34
  disabled: composer.body.trim() === "" || publishing,
38
35
  onClick: publish
39
36
  }, PUBLISH_LABEL));
40
37
  });
38
+ function usePublish() {
39
+ const client = useMapleClient();
40
+ const scope = useComposerScope("Maple.Actions");
41
+ return () => {
42
+ client.publish().then(() => scope.clear(), () => void 0);
43
+ };
44
+ }
41
45
  function reading(id, client) {
42
46
  const status = client.getState().comments.find((one) => one.id === id)?.status;
43
47
  const done = status === "resolved";
@@ -57,4 +61,4 @@ function moveOn(id, done, client) {
57
61
  }, done ? VIEW_LABELS.reopen : VIEW_LABELS.resolve);
58
62
  }
59
63
  //#endregion
60
- export { KEEP_LABEL, MapleActions, PUBLISH_LABEL, VIEW_LABELS };
64
+ export { MapleActions, PUBLISH_LABEL, SAVE_DRAFT_LABEL, VIEW_LABELS, usePublish };
@@ -1,4 +1,5 @@
1
1
  import { Slot, composeRefs } from "../slot.js";
2
+ import { usePublish } from "./actions.js";
2
3
  import { searchEmoji, shortcodeAt, writeAt } from "./emoji.js";
3
4
  import { EmojiGrid, MapleEmoji } from "./picker-emoji.js";
4
5
  import { createElement, forwardRef, useEffect, useRef, useState } from "react";
@@ -6,8 +7,9 @@ import { useMaple, useMapleClient } from "@maple-kit/react";
6
7
  //#region src/composer/body.ts
7
8
  const COMPOSER_PLACEHOLDER = "What is wrong with this?";
8
9
  const MapleBody = forwardRef(function MapleBody(props, ref) {
9
- const { composer } = useMaple();
10
+ const { composer, publishing } = useMaple();
10
11
  const client = useMapleClient();
12
+ const publish = usePublish();
11
13
  const field = useRef(null);
12
14
  const [open, setOpen] = useState(false);
13
15
  const suggest = useSuggestions(composer.body);
@@ -45,7 +47,11 @@ const MapleBody = forwardRef(function MapleBody(props, ref) {
45
47
  client.setBody(event.target.value);
46
48
  suggest.read(event.target.value, event.target.selectionStart);
47
49
  },
48
- onKeyDown: (event) => suggest.onKeyDown(event, write),
50
+ onKeyDown: (event) => {
51
+ if (!isSend(event)) return suggest.onKeyDown(event, write);
52
+ event.preventDefault();
53
+ if (composer.body.trim() !== "" && !publishing) publish();
54
+ },
49
55
  onBlur: () => suggest.clear()
50
56
  });
51
57
  return createElement("div", { className: "mk-composer-row mk-field-wrap" }, area, props.emoji === false ? null : createElement(MapleEmoji, {
@@ -103,6 +109,10 @@ function useSuggestions(body) {
103
109
  onKeyDown
104
110
  };
105
111
  }
112
+ function isSend(event) {
113
+ if (event.key !== "Enter" || event.shiftKey || event.altKey) return false;
114
+ return event.metaKey || event.ctrlKey;
115
+ }
106
116
  function stepFor(key) {
107
117
  if (key === "ArrowDown" || key === "ArrowRight") return 1;
108
118
  return key === "ArrowUp" || key === "ArrowLeft" ? -1 : 0;
@@ -1,5 +1,5 @@
1
1
  import { SHEET_BREAKPOINT_PX } from "../tokens.js";
2
- import { KEEP_LABEL, MapleActions, MapleActionsProps, PUBLISH_LABEL } from "./actions.js";
2
+ import { MapleActions, MapleActionsProps, PUBLISH_LABEL, SAVE_DRAFT_LABEL } from "./actions.js";
3
3
  import { ATTACH_WORDS, MapleAttachments, MapleAttachmentsProps } from "./attachments.js";
4
4
  import { CONTEXT_LABELS, MapleContextBadge, MapleContextProps } from "./badge.js";
5
5
  import { COMPOSER_PLACEHOLDER, MapleBody, MapleBodyProps } from "./body.js";
@@ -13,4 +13,4 @@ import { KIND_WORDS, LEAVE_DISCARD, LEAVE_KEEP, QUOTE_LIMIT, TARGET_PREFIX, UNNA
13
13
  import { EMOJI_COPY, EMOJI_ICON_PX, EmojiGrid, EmojiGridProps, MapleEmoji, MapleEmojiProps } from "./picker-emoji.js";
14
14
  import { KIND_AUTO, KIND_LABEL, MapleScoreCard, MapleScoreProps, SCORE_LABEL } from "./score.js";
15
15
  import { CLOSE_LABEL, DETAIL_ATTRIBUTE, MapleTarget, MapleTargetProps } from "./target.js";
16
- export { ATTACH_WORDS, MapleActions as Actions, MapleAttachments as Attachments, MapleBody as Body, CLOSE_LABEL, COMPOSER_LABEL, COMPOSER_PLACEHOLDER, CONTEXT_LABELS, MapleComposer as Composer, ComposerScopeError, MapleContextBadge as Context, DETAIL_ATTRIBUTE, DETENT_LABELS, MapleDetail as Detail, EMOJI_COPY, EMOJI_ICON_PX, MapleEmoji as Emoji, type EmojiChoice, EmojiGrid, type EmojiGridProps, KEEP_LABEL, KIND_AUTO, KIND_LABEL, KIND_WORDS, LEAVE_DISCARD, LEAVE_KEEP, type LeaveAsk, MapleActions, type MapleActionsProps, MapleAttachments, type MapleAttachmentsProps, MapleBody, type MapleBodyProps, MapleComposer, type MapleComposerProps, MapleContextBadge, type MapleContextProps, MapleDetail, type MapleDetailProps, MapleEmoji, type MapleEmojiProps, MapleScoreCard, type MapleScoreProps, MapleTarget, type MapleTargetProps, PUBLISH_LABEL, QUOTE_LIMIT, REVIEW_EMOJI, SCORE_LABEL, SHEET_BREAKPOINT_PX, SHORTCODE_LIMIT, MapleScoreCard as Score, type SheetDetent, type Shortcode, TARGET_PREFIX, MapleTarget as Target, UNNAMED_TARGET, type Written, appended, createLeaveAsk, detailRows, leaveMessage, peeks, quotedText, searchEmoji, shortcodeAt, targetName, targetPhrase, writeAt };
16
+ export { ATTACH_WORDS, MapleActions as Actions, MapleAttachments as Attachments, MapleBody as Body, CLOSE_LABEL, COMPOSER_LABEL, COMPOSER_PLACEHOLDER, CONTEXT_LABELS, MapleComposer as Composer, ComposerScopeError, MapleContextBadge as Context, DETAIL_ATTRIBUTE, DETENT_LABELS, MapleDetail as Detail, EMOJI_COPY, EMOJI_ICON_PX, MapleEmoji as Emoji, type EmojiChoice, EmojiGrid, type EmojiGridProps, KIND_AUTO, KIND_LABEL, KIND_WORDS, LEAVE_DISCARD, LEAVE_KEEP, type LeaveAsk, MapleActions, type MapleActionsProps, MapleAttachments, type MapleAttachmentsProps, MapleBody, type MapleBodyProps, MapleComposer, type MapleComposerProps, MapleContextBadge, type MapleContextProps, MapleDetail, type MapleDetailProps, MapleEmoji, type MapleEmojiProps, MapleScoreCard, type MapleScoreProps, MapleTarget, type MapleTargetProps, PUBLISH_LABEL, QUOTE_LIMIT, REVIEW_EMOJI, SAVE_DRAFT_LABEL, SCORE_LABEL, SHEET_BREAKPOINT_PX, SHORTCODE_LIMIT, MapleScoreCard as Score, type SheetDetent, type Shortcode, TARGET_PREFIX, MapleTarget as Target, UNNAMED_TARGET, type Written, appended, createLeaveAsk, detailRows, leaveMessage, peeks, quotedText, searchEmoji, shortcodeAt, targetName, targetPhrase, writeAt };
@@ -1,6 +1,6 @@
1
1
  import { SHEET_BREAKPOINT_PX } from "../tokens.js";
2
2
  import { ComposerScopeError } from "./scope.js";
3
- import { KEEP_LABEL, MapleActions, PUBLISH_LABEL } from "./actions.js";
3
+ import { MapleActions, PUBLISH_LABEL, SAVE_DRAFT_LABEL } from "./actions.js";
4
4
  import { ATTACH_WORDS, MapleAttachments } from "./attachments.js";
5
5
  import { CONTEXT_LABELS, MapleContextBadge } from "./badge.js";
6
6
  import { REVIEW_EMOJI, SHORTCODE_LIMIT, appended, searchEmoji, shortcodeAt, writeAt } from "./emoji.js";
@@ -13,4 +13,4 @@ import { MapleDetail, detailRows } from "./detail.js";
13
13
  import { createLeaveAsk } from "./leave.js";
14
14
  import { KIND_AUTO, KIND_LABEL, MapleScoreCard, SCORE_LABEL } from "./score.js";
15
15
  import { CLOSE_LABEL, DETAIL_ATTRIBUTE, MapleTarget } from "./target.js";
16
- export { ATTACH_WORDS, MapleActions as Actions, MapleAttachments as Attachments, MapleBody as Body, CLOSE_LABEL, COMPOSER_LABEL, COMPOSER_PLACEHOLDER, CONTEXT_LABELS, MapleComposer as Composer, ComposerScopeError, MapleContextBadge as Context, DETAIL_ATTRIBUTE, DETENT_LABELS, MapleDetail as Detail, EMOJI_COPY, EMOJI_ICON_PX, MapleEmoji as Emoji, EmojiGrid, KEEP_LABEL, KIND_AUTO, KIND_LABEL, KIND_WORDS, LEAVE_DISCARD, LEAVE_KEEP, MapleActions, MapleAttachments, MapleBody, MapleComposer, MapleContextBadge, MapleDetail, MapleEmoji, MapleScoreCard, MapleTarget, PUBLISH_LABEL, QUOTE_LIMIT, REVIEW_EMOJI, SCORE_LABEL, SHEET_BREAKPOINT_PX, SHORTCODE_LIMIT, MapleScoreCard as Score, TARGET_PREFIX, MapleTarget as Target, UNNAMED_TARGET, appended, createLeaveAsk, detailRows, leaveMessage, peeks, quotedText, searchEmoji, shortcodeAt, targetName, targetPhrase, writeAt };
16
+ export { ATTACH_WORDS, MapleActions as Actions, MapleAttachments as Attachments, MapleBody as Body, CLOSE_LABEL, COMPOSER_LABEL, COMPOSER_PLACEHOLDER, CONTEXT_LABELS, MapleComposer as Composer, ComposerScopeError, MapleContextBadge as Context, DETAIL_ATTRIBUTE, DETENT_LABELS, MapleDetail as Detail, EMOJI_COPY, EMOJI_ICON_PX, MapleEmoji as Emoji, EmojiGrid, KIND_AUTO, KIND_LABEL, KIND_WORDS, LEAVE_DISCARD, LEAVE_KEEP, MapleActions, MapleAttachments, MapleBody, MapleComposer, MapleContextBadge, MapleDetail, MapleEmoji, MapleScoreCard, MapleTarget, PUBLISH_LABEL, QUOTE_LIMIT, REVIEW_EMOJI, SAVE_DRAFT_LABEL, SCORE_LABEL, SHEET_BREAKPOINT_PX, SHORTCODE_LIMIT, MapleScoreCard as Score, TARGET_PREFIX, MapleTarget as Target, UNNAMED_TARGET, appended, createLeaveAsk, detailRows, leaveMessage, peeks, quotedText, searchEmoji, shortcodeAt, targetName, targetPhrase, writeAt };
package/dist/hover.js ADDED
@@ -0,0 +1,17 @@
1
+ //#region src/hover.ts
2
+ function within(event) {
3
+ const other = event.relatedTarget;
4
+ return other instanceof Node && event.currentTarget.contains(other);
5
+ }
6
+ function hoverHandlers(enter, leave) {
7
+ return {
8
+ onPointerOver: (event) => {
9
+ if (!within(event)) enter();
10
+ },
11
+ onPointerOut: (event) => {
12
+ if (!within(event)) leave();
13
+ }
14
+ };
15
+ }
16
+ //#endregion
17
+ export { hoverHandlers };
@@ -7,6 +7,7 @@ import { Tip } from "../tip.js";
7
7
  import { renderPart } from "../part.js";
8
8
  import { ISLAND_COPY, kindPhrase } from "./language.js";
9
9
  import { useIsland } from "./context.js";
10
+ import { hoverHandlers } from "../hover.js";
10
11
  import { MapleLeaf } from "../marks/shape.js";
11
12
  import { absoluteTime, relativeTime } from "./time.js";
12
13
  import { createElement, forwardRef, useCallback, useRef, useState } from "react";
@@ -31,8 +32,7 @@ const Item = forwardRef(function Item(props, ref) {
31
32
  "data-mk-selected": String(selected),
32
33
  className: cx("mk-row", className),
33
34
  onClick: () => client.viewComment(comment.id),
34
- onPointerEnter: () => client.peek(comment.id),
35
- onPointerLeave: () => client.peek(null),
35
+ ...hoverHandlers(() => client.peek(comment.id), () => client.peek(null)),
36
36
  onFocus: () => client.peek(comment.id),
37
37
  onBlur: () => client.peek(null),
38
38
  ref: composeRefs(ref, useReveal(selected))
@@ -0,0 +1,32 @@
1
+ import { Box } from "./geometry.js";
2
+ //#region src/marks/clearance.d.ts
3
+ /** One of the four corners, as the two flips that reach it. */
4
+ export interface LabelSpot {
5
+ readonly below: boolean;
6
+ readonly end: boolean;
7
+ }
8
+ /**
9
+ * In the order they are tried. Above-left is where a reader looks for the name
10
+ * of the thing under it; staying on the anchor's own edge is worth more than
11
+ * staying above it, so the far end of the ring is tried only after both sides.
12
+ */
13
+ export declare const LABEL_SPOTS: readonly LabelSpot[];
14
+ /** The ring and the label as it is currently drawn. */
15
+ export interface LabelPlacement {
16
+ readonly ring: Box;
17
+ /** Measured, so the size and the gaps are the ones the stylesheet gives it. */
18
+ readonly label: Box;
19
+ /** The corner it is measured at, which is what makes those gaps readable. */
20
+ readonly drawn: LabelSpot;
21
+ }
22
+ /** The box the label would take at one corner, keeping the gaps it is drawn with. */
23
+ export declare function labelBox(at: LabelPlacement, spot: LabelSpot): Box;
24
+ /** True when the page paints a line of its own text anywhere inside the box. */
25
+ export declare function pageTextIn(box: Box, overlay: Element): boolean;
26
+ /**
27
+ * The first corner the window holds and the page has not written in. When
28
+ * every corner has text in it there is nowhere better to go, so it takes the
29
+ * first the window holds and carries a shadow to say it is floating.
30
+ */
31
+ export declare function labelSpotFor(at: LabelPlacement, overlay: Element): LabelSpot;
32
+ //#endregion
@@ -0,0 +1,72 @@
1
+ //#region src/marks/clearance.ts
2
+ const COLUMNS = [
3
+ .04,
4
+ .36,
5
+ .68,
6
+ .96
7
+ ];
8
+ const ROWS = [
9
+ .15,
10
+ .5,
11
+ .85
12
+ ];
13
+ const LABEL_SPOTS = [
14
+ {
15
+ below: false,
16
+ end: false
17
+ },
18
+ {
19
+ below: true,
20
+ end: false
21
+ },
22
+ {
23
+ below: false,
24
+ end: true
25
+ },
26
+ {
27
+ below: true,
28
+ end: true
29
+ }
30
+ ];
31
+ function labelBox(at, spot) {
32
+ const { drawn, label, ring } = at;
33
+ const far = ring.x + ring.width - label.width;
34
+ const gap = drawn.below ? label.y - (ring.y + ring.height) : ring.y - (label.y + label.height);
35
+ const edge = drawn.end ? far - label.x : label.x - ring.x;
36
+ return {
37
+ x: spot.end ? far - edge : ring.x + edge,
38
+ y: spot.below ? ring.y + ring.height + gap : ring.y - gap - label.height,
39
+ width: label.width,
40
+ height: label.height
41
+ };
42
+ }
43
+ function pageTextIn(box, overlay) {
44
+ return ROWS.some((down) => COLUMNS.some((along) => glyphAt(overlay, box.x + box.width * along, box.y + box.height * down)));
45
+ }
46
+ function pageElementAt(overlay, x, y) {
47
+ return overlay.ownerDocument.elementsFromPoint(x, y).find((one) => !overlay.contains(one));
48
+ }
49
+ function glyphAt(overlay, x, y) {
50
+ const found = pageElementAt(overlay, x, y);
51
+ if (!found) return false;
52
+ const line = overlay.ownerDocument.createRange();
53
+ return [...found.childNodes].some((node) => {
54
+ if (node.nodeType !== Node.TEXT_NODE || (node.textContent ?? "").trim() === "") return false;
55
+ line.selectNodeContents(node);
56
+ return [...line.getClientRects()].some((rect) => inside(rect, x, y));
57
+ });
58
+ }
59
+ function inside(rect, x, y) {
60
+ return x >= rect.left && x <= rect.right && y >= rect.top && y <= rect.bottom;
61
+ }
62
+ function labelSpotFor(at, overlay) {
63
+ const view = overlay.ownerDocument.defaultView?.innerHeight ?? 0;
64
+ const tried = LABEL_SPOTS.map((spot) => ({
65
+ spot,
66
+ box: labelBox(at, spot)
67
+ }));
68
+ const held = tried.filter(({ box }) => box.y >= 0 && box.y + box.height <= view);
69
+ return (held.find(({ box }) => !pageTextIn(box, overlay)) ?? held[0] ?? tried[0]).spot;
70
+ }
71
+ //#endregion
72
+ export { LABEL_SPOTS, labelBox, labelSpotFor, pageTextIn };
package/dist/marks/css.js CHANGED
@@ -253,7 +253,9 @@ function ringCss() {
253
253
 
254
254
 
255
255
  /* Anchored to the ring's edge rather than offset by a number: the label is two
256
- lines in developer detail and one line the rest of the time. */
256
+ lines in developer detail and one line the rest of the time. The shadow is
257
+ for the anchor with text in all four of its corners, where the label has to
258
+ cover something and has to read as floating over it rather than part of it. */
257
259
  .mk-ring-label {
258
260
  position: absolute;
259
261
  bottom: 100%;
@@ -265,6 +267,7 @@ function ringCss() {
265
267
  padding: 1px 7px 2px;
266
268
  border-radius: var(--mk-r-xs) var(--mk-r-xs) var(--mk-r-xs) 0;
267
269
  background: var(--mk-accent);
270
+ box-shadow: var(--mk-sh2);
268
271
  color: var(--mk-accent-ink);
269
272
  font-size: 10px;
270
273
  font-weight: 600;
@@ -277,6 +280,18 @@ function ringCss() {
277
280
  border-radius: 0 var(--mk-r-xs) var(--mk-r-xs) var(--mk-r-xs);
278
281
  }
279
282
 
283
+ /* The square corner is the one against the ring, so the label always points
284
+ back at what it names, from whichever of the four corners it took. */
285
+ .mk-ring-label[data-mk-end] {
286
+ right: -2px;
287
+ left: auto;
288
+ border-radius: var(--mk-r-xs) var(--mk-r-xs) 0 var(--mk-r-xs);
289
+ }
290
+
291
+ .mk-ring-label[data-mk-below][data-mk-end] {
292
+ border-radius: var(--mk-r-xs) 0 var(--mk-r-xs) var(--mk-r-xs);
293
+ }
294
+
280
295
  .mk-ring-name,
281
296
  .mk-ring-note {
282
297
  display: block;
@@ -1,7 +1,8 @@
1
1
  import { AvatarProps, MapleAvatar, initialsOf } from "./avatar.js";
2
+ import { Box, COLLISION_GAP_PX, COLLISION_MAX_TRIES, COLLISION_STEP_PX, MARK_HIT_PX, MARK_OFFSET_PX, MARK_SIZE_PX, Waterline, culled, markSpot, placeMark, ringBox, runBox, waterline } from "./geometry.js";
3
+ import { LABEL_SPOTS, LabelPlacement, LabelSpot, labelBox, labelSpotFor, pageTextIn } from "./clearance.js";
2
4
  import { marksCss } from "./css.js";
3
5
  import { Paint, startFrameLoop, useFrameLoop, viewportHeight } from "./frame.js";
4
- import { Box, COLLISION_GAP_PX, COLLISION_MAX_TRIES, COLLISION_STEP_PX, MARK_HIT_PX, MARK_OFFSET_PX, MARK_SIZE_PX, Waterline, culled, markSpot, placeMark, ringBox, runBox, waterline } from "./geometry.js";
5
6
  import { NOTHING_NAMED, RingLabel, kindPhrase, markLabel, markTitle, ringLabel } from "./label.js";
6
7
  import { MapleMarkLayer, MarkLayerProps } from "./layer.js";
7
8
  import { LEAF_OUTLINE, LEAF_ROTATION, LEAF_SOLID, LEAF_VIEW_BOX } from "./leaf.js";
@@ -9,7 +10,7 @@ import { MapleMark, MarkProps } from "./mark.js";
9
10
  import { NUDGE_LIMIT_PX, NUDGE_THRESHOLD_PX, Nudge, NudgeHandlers, NudgePointer, Nudges, clampNudge, dragged, useNudges } from "./nudge.js";
10
11
  import { MOVING_ATTRIBUTE, OFF_ATTRIBUTE, flag, place } from "./paint.js";
11
12
  import { PIXEL_LEAF_SHADES, PIXEL_LEAF_VIEW_BOX } from "./pixel-leaf.js";
12
- import { Placement, addresses, placements } from "./placement.js";
13
- import { BELOW_ATTRIBUTE, MapleTargetRing, RingState, TargetRingProps } from "./ring.js";
13
+ import { DraftPlacement, Located, Placement, addresses, placements } from "./placement.js";
14
+ import { BELOW_ATTRIBUTE, END_ATTRIBUTE, MapleTargetRing, RingState, TargetRingProps } from "./ring.js";
14
15
  import { HALF, LeafProps, MapleLeaf, nextClipId } from "./shape.js";
15
- export { MapleAvatar as Avatar, type AvatarProps, BELOW_ATTRIBUTE, type Box, COLLISION_GAP_PX, COLLISION_MAX_TRIES, COLLISION_STEP_PX, HALF, LEAF_OUTLINE, LEAF_ROTATION, LEAF_SOLID, LEAF_VIEW_BOX, MapleLeaf as Leaf, type LeafProps, MARK_HIT_PX, MARK_OFFSET_PX, MARK_SIZE_PX, MOVING_ATTRIBUTE, MapleAvatar, MapleLeaf, MapleMark, MapleMarkLayer, MapleTargetRing, MapleMark as Mark, MapleMarkLayer as MarkLayer, type MarkLayerProps, type MarkProps, NOTHING_NAMED, NUDGE_LIMIT_PX, NUDGE_THRESHOLD_PX, type Nudge, type NudgeHandlers, type NudgePointer, type Nudges, OFF_ATTRIBUTE, PIXEL_LEAF_SHADES, PIXEL_LEAF_VIEW_BOX, type Paint, type Placement, type RingLabel, type RingState, MapleTargetRing as TargetRing, type TargetRingProps, type Waterline, addresses, clampNudge, culled, dragged, flag, initialsOf, kindPhrase, markLabel, markSpot, markTitle, marksCss, nextClipId, place, placeMark, placements, ringBox, ringLabel, runBox, startFrameLoop, useFrameLoop, useNudges, viewportHeight, waterline };
16
+ export { MapleAvatar as Avatar, type AvatarProps, BELOW_ATTRIBUTE, type Box, COLLISION_GAP_PX, COLLISION_MAX_TRIES, COLLISION_STEP_PX, type DraftPlacement, END_ATTRIBUTE, HALF, LABEL_SPOTS, LEAF_OUTLINE, LEAF_ROTATION, LEAF_SOLID, LEAF_VIEW_BOX, type LabelPlacement, type LabelSpot, MapleLeaf as Leaf, type LeafProps, type Located, MARK_HIT_PX, MARK_OFFSET_PX, MARK_SIZE_PX, MOVING_ATTRIBUTE, MapleAvatar, MapleLeaf, MapleMark, MapleMarkLayer, MapleTargetRing, MapleMark as Mark, MapleMarkLayer as MarkLayer, type MarkLayerProps, type MarkProps, NOTHING_NAMED, NUDGE_LIMIT_PX, NUDGE_THRESHOLD_PX, type Nudge, type NudgeHandlers, type NudgePointer, type Nudges, OFF_ATTRIBUTE, PIXEL_LEAF_SHADES, PIXEL_LEAF_VIEW_BOX, type Paint, type Placement, type RingLabel, type RingState, MapleTargetRing as TargetRing, type TargetRingProps, type Waterline, addresses, clampNudge, culled, dragged, flag, initialsOf, kindPhrase, labelBox, labelSpotFor, markLabel, markSpot, markTitle, marksCss, nextClipId, pageTextIn, place, placeMark, placements, ringBox, ringLabel, runBox, startFrameLoop, useFrameLoop, useNudges, viewportHeight, waterline };
@@ -4,12 +4,13 @@ import { COLLISION_GAP_PX, COLLISION_MAX_TRIES, COLLISION_STEP_PX, MARK_HIT_PX,
4
4
  import { LEAF_OUTLINE, LEAF_ROTATION, LEAF_SOLID, LEAF_VIEW_BOX } from "./leaf.js";
5
5
  import { HALF, MapleLeaf, nextClipId } from "./shape.js";
6
6
  import { MapleAvatar, initialsOf } from "./avatar.js";
7
+ import { LABEL_SPOTS, labelBox, labelSpotFor, pageTextIn } from "./clearance.js";
7
8
  import { startFrameLoop, useFrameLoop, viewportHeight } from "./frame.js";
8
9
  import { NOTHING_NAMED, kindPhrase, markLabel, markTitle, ringLabel } from "./label.js";
9
10
  import { MapleMark } from "./mark.js";
10
11
  import { NUDGE_LIMIT_PX, NUDGE_THRESHOLD_PX, clampNudge, dragged, useNudges } from "./nudge.js";
11
12
  import { MOVING_ATTRIBUTE, OFF_ATTRIBUTE, flag, place } from "./paint.js";
12
13
  import { addresses, placements } from "./placement.js";
13
- import { BELOW_ATTRIBUTE, MapleTargetRing } from "./ring.js";
14
+ import { BELOW_ATTRIBUTE, END_ATTRIBUTE, MapleTargetRing } from "./ring.js";
14
15
  import { MapleMarkLayer } from "./layer.js";
15
- export { MapleAvatar as Avatar, BELOW_ATTRIBUTE, COLLISION_GAP_PX, COLLISION_MAX_TRIES, COLLISION_STEP_PX, HALF, LEAF_OUTLINE, LEAF_ROTATION, LEAF_SOLID, LEAF_VIEW_BOX, MapleLeaf as Leaf, MARK_HIT_PX, MARK_OFFSET_PX, MARK_SIZE_PX, MOVING_ATTRIBUTE, MapleAvatar, MapleLeaf, MapleMark, MapleMarkLayer, MapleTargetRing, MapleMark as Mark, MapleMarkLayer as MarkLayer, NOTHING_NAMED, NUDGE_LIMIT_PX, NUDGE_THRESHOLD_PX, OFF_ATTRIBUTE, PIXEL_LEAF_SHADES, PIXEL_LEAF_VIEW_BOX, MapleTargetRing as TargetRing, addresses, clampNudge, culled, dragged, flag, initialsOf, kindPhrase, markLabel, markSpot, markTitle, marksCss, nextClipId, place, placeMark, placements, ringBox, ringLabel, runBox, startFrameLoop, useFrameLoop, useNudges, viewportHeight, waterline };
16
+ export { MapleAvatar as Avatar, BELOW_ATTRIBUTE, COLLISION_GAP_PX, COLLISION_MAX_TRIES, COLLISION_STEP_PX, END_ATTRIBUTE, HALF, LABEL_SPOTS, LEAF_OUTLINE, LEAF_ROTATION, LEAF_SOLID, LEAF_VIEW_BOX, MapleLeaf as Leaf, MARK_HIT_PX, MARK_OFFSET_PX, MARK_SIZE_PX, MOVING_ATTRIBUTE, MapleAvatar, MapleLeaf, MapleMark, MapleMarkLayer, MapleTargetRing, MapleMark as Mark, MapleMarkLayer as MarkLayer, NOTHING_NAMED, NUDGE_LIMIT_PX, NUDGE_THRESHOLD_PX, OFF_ATTRIBUTE, PIXEL_LEAF_SHADES, PIXEL_LEAF_VIEW_BOX, MapleTargetRing as TargetRing, addresses, clampNudge, culled, dragged, flag, initialsOf, kindPhrase, labelBox, labelSpotFor, markLabel, markSpot, markTitle, marksCss, nextClipId, pageTextIn, place, placeMark, placements, ringBox, ringLabel, runBox, startFrameLoop, useFrameLoop, useNudges, viewportHeight, waterline };
@@ -17,7 +17,10 @@ export declare function kindPhrase(kind: PickKind | undefined, human: string | u
17
17
  /** The ring's words: the application's name for the thing, in a phrase. */
18
18
  export declare function ringLabel(label: RingLabel): string;
19
19
  /** The mark's tooltip: who, where in its life, and what it is on. */
20
- export declare function markTitle(author: string | undefined, status: CommentStatus, on?: string): string;
21
- /** The mark's accessible name. The address comes first, because it is the address. */
22
- export declare function markLabel(address: number, author: string | undefined, status: CommentStatus): string;
20
+ export declare function markTitle(author: string | undefined, status: CommentStatus | "draft", on?: string): string;
21
+ /**
22
+ * The mark's accessible name. The address comes first, because it is the
23
+ * address; a draft has none yet, and says it is one instead.
24
+ */
25
+ export declare function markLabel(address: number | undefined, author: string | undefined, status: CommentStatus): string;
23
26
  //#endregion
@@ -17,15 +17,18 @@ function ringLabel(label) {
17
17
  function joined(parts) {
18
18
  return parts.filter(Boolean).join(" · ");
19
19
  }
20
+ const DRAFT_LABEL = "Draft";
20
21
  function markTitle(author, status, on) {
21
22
  return joined([
22
23
  author,
23
- STATUS_LABELS[status],
24
+ status === "draft" ? DRAFT_LABEL : STATUS_LABELS[status],
24
25
  on
25
26
  ]);
26
27
  }
27
28
  function markLabel(address, author, status) {
28
- return `Comment ${address}${author ? ` by ${author}` : ""}, ${STATUS_LABELS[status]}`;
29
+ const who = author ? ` by ${author}` : "";
30
+ if (address === void 0) return `${DRAFT_LABEL} comment${who}`;
31
+ return `Comment ${address}${who}, ${STATUS_LABELS[status]}`;
29
32
  }
30
33
  //#endregion
31
- export { NOTHING_NAMED, kindPhrase, markLabel, markTitle, ringLabel };
34
+ export { DRAFT_LABEL, NOTHING_NAMED, kindPhrase, markLabel, markTitle, ringLabel };
@@ -1,11 +1,12 @@
1
1
  import { useMapleUi } from "../context.js";
2
+ import { hoverHandlers } from "../hover.js";
2
3
  import { culled, markSpot, placeMark } from "./geometry.js";
3
4
  import { useFrameLoop, viewportHeight } from "./frame.js";
4
5
  import { ringLabel } from "./label.js";
5
6
  import { MapleMark } from "./mark.js";
6
7
  import { useNudges } from "./nudge.js";
7
8
  import { OFF_ATTRIBUTE, flag, place } from "./paint.js";
8
- import { addresses, placements } from "./placement.js";
9
+ import { addresses, draftPlacements, placements } from "./placement.js";
9
10
  import { MapleTargetRing } from "./ring.js";
10
11
  import { createElement, forwardRef, useCallback, useEffect, useMemo, useRef } from "react";
11
12
  import { useMaple, useMapleClient } from "@maple-kit/react";
@@ -32,15 +33,17 @@ const MapleMarkLayer = forwardRef(function MapleMarkLayer(props, ref) {
32
33
  address,
33
34
  container
34
35
  ]);
36
+ const drafts = useWaiting(state);
37
+ const drafted = useMemo(() => draftPlacements(drafts, container.ownerDocument), [drafts, container]);
35
38
  const nudges = useNudges();
36
39
  const nodes = useRef(/* @__PURE__ */ new Map());
37
40
  const paint = useCallback(() => {
38
41
  const height = viewportHeight(container);
39
42
  const taken = [];
40
- for (const placement of placed) {
41
- const node = nodes.current.get(placement.comment.id);
42
- const moved = nudges.of(placement.comment.id);
43
- if (node) taken.push(step(node, placement, {
43
+ for (const [id, located] of everything(placed, drafted)) {
44
+ const node = nodes.current.get(id);
45
+ const moved = nudges.of(id);
46
+ if (node) taken.push(step(node, located, {
44
47
  height,
45
48
  taken,
46
49
  ...moved ? { moved } : {}
@@ -48,6 +51,7 @@ const MapleMarkLayer = forwardRef(function MapleMarkLayer(props, ref) {
48
51
  }
49
52
  }, [
50
53
  container,
54
+ drafted,
51
55
  nudges,
52
56
  placed
53
57
  ]);
@@ -63,10 +67,7 @@ const MapleMarkLayer = forwardRef(function MapleMarkLayer(props, ref) {
63
67
  key: placement.comment.id,
64
68
  ref: keep(nodes.current, placement.comment.id),
65
69
  onClick: () => onSelect(placement.comment),
66
- onPointerEnter: () => client.peek(placement.comment.id),
67
- onPointerLeave: () => client.peek(null),
68
- onFocus: () => client.peek(placement.comment.id),
69
- onBlur: () => client.peek(null)
70
+ ...pointing(client, placement.comment.id)
70
71
  })), [
71
72
  client,
72
73
  nudges,
@@ -75,12 +76,29 @@ const MapleMarkLayer = forwardRef(function MapleMarkLayer(props, ref) {
75
76
  placed,
76
77
  selectedId
77
78
  ]);
79
+ const unsent = useMemo(() => drafted.map((placement) => createElement(MapleMark, {
80
+ ...draftProps(placement, {
81
+ peeked,
82
+ nudges
83
+ }),
84
+ ...nudges.handlersFor(placement.draft.id),
85
+ key: placement.draft.id,
86
+ ref: keep(nodes.current, placement.draft.id),
87
+ onClick: () => client.resumeDraft(placement.draft.id),
88
+ ...pointing(client, placement.draft.id)
89
+ })), [
90
+ client,
91
+ drafted,
92
+ nudges,
93
+ peeked
94
+ ]);
78
95
  return createElement("div", {
79
96
  className: className ? `mk-marks ${className}` : "mk-marks",
80
97
  ref
81
- }, ...marks, createElement(MapleTargetRing, ringFor({
98
+ }, ...unsent, ...marks, createElement(MapleTargetRing, ringFor({
82
99
  client: state,
83
100
  placed,
101
+ drafted,
84
102
  pointing: {
85
103
  peeked,
86
104
  selected: selectedId
@@ -98,6 +116,21 @@ function useScrollTo(placed, selectedId) {
98
116
  });
99
117
  }, [target]);
100
118
  }
119
+ function useWaiting(state) {
120
+ const { composer, drafts } = state;
121
+ const writing = composer.open ? composer.draftId : void 0;
122
+ return useMemo(() => writing === void 0 ? drafts : drafts.filter((draft) => draft.id !== writing), [drafts, writing]);
123
+ }
124
+ function everything(placed, drafted) {
125
+ return [...placed.map((one) => [one.comment.id, one]), ...drafted.map((one) => [one.draft.id, one])];
126
+ }
127
+ function pointing(client, id) {
128
+ return {
129
+ ...hoverHandlers(() => client.peek(id), () => client.peek(null)),
130
+ onFocus: () => client.peek(id),
131
+ onBlur: () => client.peek(null)
132
+ };
133
+ }
101
134
  function boxOf(placement) {
102
135
  const box = placement.range?.getBoundingClientRect() ?? placement.element.getBoundingClientRect();
103
136
  return placement.region === void 0 ? box : regionBox(box, placement.region);
@@ -139,6 +172,20 @@ function markProps(placement, view) {
139
172
  ...comment.author.colorSlot === void 0 ? {} : { colorSlot: comment.author.colorSlot }
140
173
  };
141
174
  }
175
+ function draftProps(placement, view) {
176
+ const { draft } = placement;
177
+ return {
178
+ sent: false,
179
+ confidence: placement.confidence,
180
+ on: ringLabel({
181
+ kind: kindOf(draft.anchor),
182
+ element: placement.element
183
+ }),
184
+ peeked: draft.id === view.peeked,
185
+ nudged: view.nudges.of(draft.id) !== void 0,
186
+ dragging: view.nudges.dragging === draft.id
187
+ };
188
+ }
142
189
  function ringFor(input) {
143
190
  const peeked = marked(input, input.pointing.peeked, "hovered");
144
191
  if (peeked) return peeked;
@@ -147,22 +194,35 @@ function ringFor(input) {
147
194
  return marked(input, input.pointing.selected, "selected") ?? {};
148
195
  }
149
196
  function marked(input, id, state) {
150
- const hit = input.placed.find((placement) => placement.comment.id === id);
197
+ const hit = drawn(input, id);
151
198
  if (!hit) return void 0;
152
199
  return {
153
200
  target: hit.range ?? hit.element,
154
201
  ...hit.region === void 0 ? {} : { region: hit.region },
155
202
  label: ringLabel({
156
- kind: kindOf(hit.comment.anchor),
203
+ kind: kindOf(hit.anchor),
157
204
  element: hit.element
158
205
  }),
159
206
  ...noteFor({
160
- anchor: hit.comment.anchor,
207
+ anchor: hit.anchor,
161
208
  element: hit.element
162
209
  }, input.client.detail),
163
210
  state
164
211
  };
165
212
  }
213
+ function drawn(input, id) {
214
+ if (id === void 0) return void 0;
215
+ const comment = input.placed.find((placement) => placement.comment.id === id);
216
+ if (comment) return {
217
+ ...comment,
218
+ anchor: comment.comment.anchor
219
+ };
220
+ const draft = input.drafted.find((placement) => placement.draft.id === id);
221
+ return draft ? {
222
+ ...draft,
223
+ anchor: draft.draft.anchor
224
+ } : void 0;
225
+ }
166
226
  function composing(target, input) {
167
227
  const found = resolveAnchor(target.anchor, {
168
228
  root: input.root,
@@ -4,8 +4,8 @@ import { CommentStatus } from "@maple-kit/core";
4
4
  //#region src/marks/mark.d.ts
5
5
  /** Everything the mark says, and nothing about how it should look. */
6
6
  export interface MarkProps extends AsChildProps, ButtonHTMLAttributes<HTMLButtonElement> {
7
- /** The address: the number the list and the export table also show. */
8
- readonly address: number;
7
+ /** The address: the number the list and the export table also show. A draft has none. */
8
+ readonly address?: number;
9
9
  readonly status?: CommentStatus;
10
10
  /** False for a comment still being written, which is the dashed form. */
11
11
  readonly sent?: boolean;
@@ -27,13 +27,13 @@ const MapleMark = forwardRef(function MapleMark(props, ref) {
27
27
  }),
28
28
  "aria-label": markLabel(address, author, status),
29
29
  "aria-pressed": selected ?? false,
30
- title: markTitle(author, status, on),
30
+ title: markTitle(author, sent === false ? "draft" : status, on),
31
31
  className: className ? `mk-mark mk-hit ${className}` : "mk-mark mk-hit",
32
32
  ref: composeRefs(ref, paint)
33
33
  }, ...inside(asChild, children, [createElement(MapleLeaf, {
34
34
  key: "leaf",
35
35
  form
36
- }), createElement("span", {
36
+ }), address === void 0 ? null : createElement("span", {
37
37
  key: "n",
38
38
  className: "mk-mark-n mk-num",
39
39
  "data-mk-digits": String(String(address).length)
@@ -1,11 +1,9 @@
1
+ import { Draft } from "@maple-kit/core/overlay";
1
2
  import { AnchorRegion } from "@maple-kit/core/anchor";
2
3
  import { Comment } from "@maple-kit/core";
3
4
  //#region src/marks/placement.d.ts
4
- /** A comment the page still has somewhere to put. */
5
- export interface Placement {
6
- readonly comment: Comment;
7
- /** The address: its place in the branch's own order, counted from one. */
8
- readonly address: number;
5
+ /** Where on the page something is, whether it was published or not. */
6
+ export interface Located {
9
7
  readonly element: Element;
10
8
  /** The passage itself, when a text rung placed it. */
11
9
  readonly range?: Range;
@@ -13,6 +11,16 @@ export interface Placement {
13
11
  readonly region?: AnchorRegion;
14
12
  readonly confidence: number;
15
13
  }
14
+ /** A comment the page still has somewhere to put. */
15
+ export interface Placement extends Located {
16
+ readonly comment: Comment;
17
+ /** The address: its place in the branch's own order, counted from one. */
18
+ readonly address: number;
19
+ }
20
+ /** A draft the page still has somewhere to put. No address until it is sent. */
21
+ export interface DraftPlacement extends Located {
22
+ readonly draft: Draft;
23
+ }
16
24
  /** The address of every comment, which the list and the export share. */
17
25
  export declare function addresses(comments: readonly Comment[]): ReadonlyMap<string, number>;
18
26
  /**
@@ -7,22 +7,37 @@ function placements(comments, address, root) {
7
7
  const placed = [];
8
8
  for (const comment of comments) {
9
9
  if (comment.status === "orphaned") continue;
10
- const passage = kindOf(comment.anchor) === "text";
11
- const found = resolveAnchor(comment.anchor, {
12
- root,
13
- passage
14
- });
15
- if (found.status !== "resolved") continue;
16
- placed.push({
10
+ const found = locate(comment.anchor, root);
11
+ if (found) placed.push({
12
+ ...found,
17
13
  comment,
18
- address: address.get(comment.id) ?? 0,
19
- element: found.element,
20
- ...found.range === void 0 ? {} : { range: found.range },
21
- ...comment.anchor.region === void 0 ? {} : { region: comment.anchor.region },
22
- confidence: found.confidence
14
+ address: address.get(comment.id) ?? 0
23
15
  });
24
16
  }
25
17
  return placed;
26
18
  }
19
+ function draftPlacements(drafts, root) {
20
+ return drafts.flatMap((draft) => {
21
+ const found = locate(draft.anchor, root);
22
+ return found ? [{
23
+ ...found,
24
+ draft
25
+ }] : [];
26
+ });
27
+ }
28
+ function locate(anchor, root) {
29
+ const passage = kindOf(anchor) === "text";
30
+ const found = resolveAnchor(anchor, {
31
+ root,
32
+ passage
33
+ });
34
+ if (found.status !== "resolved") return void 0;
35
+ return {
36
+ element: found.element,
37
+ ...found.range === void 0 ? {} : { range: found.range },
38
+ ...anchor.region === void 0 ? {} : { region: anchor.region },
39
+ confidence: found.confidence
40
+ };
41
+ }
27
42
  //#endregion
28
- export { addresses, placements };
43
+ export { addresses, draftPlacements, placements };
@@ -1,7 +1,9 @@
1
1
  import { AnchorRegion } from "@maple-kit/core/anchor";
2
2
  //#region src/marks/ring.d.ts
3
- /** Set on the label when the anchor is too near the top to sit above it. */
3
+ /** Set on the label when it was moved under the anchor to clear page text. */
4
4
  export declare const BELOW_ATTRIBUTE = "data-mk-below";
5
+ /** The same, for the move to the anchor's far end. The two combine. */
6
+ export declare const END_ATTRIBUTE = "data-mk-end";
5
7
  /**
6
8
  * Why the ring is showing. `selected` is the one that outlives the pointer:
7
9
  * a click holds the ring on the page after the hand has moved away.
@@ -1,14 +1,14 @@
1
1
  import { useMapleUi } from "../context.js";
2
2
  import { composeRefs } from "../slot.js";
3
3
  import { culled, ringBox, runBox } from "./geometry.js";
4
+ import { labelSpotFor } from "./clearance.js";
4
5
  import { useFrameLoop, viewportHeight } from "./frame.js";
5
6
  import { MOVING_ATTRIBUTE, OFF_ATTRIBUTE, flag, place } from "./paint.js";
6
7
  import { createElement, forwardRef, useCallback, useRef, useState } from "react";
7
8
  import { regionBox } from "@maple-kit/core/anchor";
8
9
  //#region src/marks/ring.ts
9
10
  const BELOW_ATTRIBUTE = "data-mk-below";
10
- const LABEL_ROOM_PX = 26;
11
- const NOTE_ROOM_PX = 40;
11
+ const END_ATTRIBUTE = "data-mk-end";
12
12
  const PART = "Maple.TargetRing";
13
13
  function linesOf(target) {
14
14
  return "startContainer" in target ? [...target.getClientRects()] : [];
@@ -18,7 +18,6 @@ function empty(rect) {
18
18
  }
19
19
  const MapleTargetRing = forwardRef(function MapleTargetRing(props, ref) {
20
20
  const { className, label, note, region, state = "composing", target } = props;
21
- const room = note === void 0 ? LABEL_ROOM_PX : NOTE_ROOM_PX;
22
21
  const { container } = useMapleUi(PART);
23
22
  const ring = useRef(null);
24
23
  const cap = useRef(null);
@@ -33,8 +32,9 @@ const MapleTargetRing = forwardRef(function MapleTargetRing(props, ref) {
33
32
  flag(node, OFF_ATTRIBUTE, away);
34
33
  flag(node, MOVING_ATTRIBUTE, moving);
35
34
  if (away) return;
36
- place(node, ringBox(rect));
37
- if (cap.current) flag(cap.current, BELOW_ATTRIBUTE, rect.y < room);
35
+ const drawn = ringBox(rect);
36
+ place(node, drawn);
37
+ if (cap.current && !moving) corner(cap.current, drawn, container);
38
38
  const boxes = region === void 0 ? linesOf(target) : [];
39
39
  if (boxes.length !== lines) setLines(boxes.length);
40
40
  boxes.forEach((box, index) => {
@@ -45,7 +45,6 @@ const MapleTargetRing = forwardRef(function MapleTargetRing(props, ref) {
45
45
  container,
46
46
  lines,
47
47
  region,
48
- room,
49
48
  target
50
49
  ]);
51
50
  useFrameLoop(PART, paint);
@@ -60,6 +59,19 @@ const MapleTargetRing = forwardRef(function MapleTargetRing(props, ref) {
60
59
  })
61
60
  }, label === void 0 ? null : caption(label, note, cap), ...run(lines, runs));
62
61
  });
62
+ function corner(cap, ring, overlay) {
63
+ const drawn = {
64
+ below: cap.hasAttribute(BELOW_ATTRIBUTE),
65
+ end: cap.hasAttribute(END_ATTRIBUTE)
66
+ };
67
+ const spot = labelSpotFor({
68
+ ring,
69
+ label: cap.getBoundingClientRect(),
70
+ drawn
71
+ }, overlay);
72
+ flag(cap, BELOW_ATTRIBUTE, spot.below);
73
+ flag(cap, END_ATTRIBUTE, spot.end);
74
+ }
63
75
  function caption(label, note, ref) {
64
76
  return createElement("span", {
65
77
  className: "mk-ring-label",
@@ -82,4 +94,4 @@ function run(lines, runs) {
82
94
  }));
83
95
  }
84
96
  //#endregion
85
- export { BELOW_ATTRIBUTE, MapleTargetRing };
97
+ export { BELOW_ATTRIBUTE, END_ATTRIBUTE, MapleTargetRing };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maple-kit/ui",
3
- "version": "0.11.0",
3
+ "version": "0.12.1",
4
4
  "description": "Maple's composed reviewer parts: the marks, the island and the composer",
5
5
  "license": "Apache-2.0",
6
6
  "type": "module",
@@ -33,9 +33,9 @@
33
33
  "./package.json": "./package.json"
34
34
  },
35
35
  "dependencies": {
36
- "@maple-kit/core": "0.11.0",
37
- "@maple-kit/react": "0.11.0",
38
- "@maple-kit/mock": "0.11.0"
36
+ "@maple-kit/core": "0.12.1",
37
+ "@maple-kit/react": "0.12.1",
38
+ "@maple-kit/mock": "0.12.1"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "react": "^18.2.0 || ^19.0.0",