@maple-kit/ui 0.10.0 → 0.12.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/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))
@@ -1,3 +1,4 @@
1
+ import { COMMENT_SHORTCUT } from "@maple-kit/core/client";
1
2
  //#region src/island/language.ts
2
3
  const FILTER_LABELS = {
3
4
  all: "All",
@@ -23,11 +24,6 @@ function tallyTitle(filter, count) {
23
24
  const many = count === 1 ? "comment" : "comments";
24
25
  return `${String(count)} ${FILTER_LABELS[filter].toLowerCase()} ${many} — click to show only these`;
25
26
  }
26
- const PICK_ORDER = [
27
- "element",
28
- "text",
29
- "region"
30
- ];
31
27
  const PICK_LABELS = {
32
28
  element: "Element",
33
29
  region: "Region",
@@ -135,8 +131,8 @@ function openLabel(count) {
135
131
  function triggerLabel(count) {
136
132
  return `Open Maple: ${openLabel(count)}`;
137
133
  }
138
- function pickTitle(kind) {
139
- return `Comment on ${kind} — press t while picking to cycle`;
134
+ function pickTitle(kind, key = COMMENT_SHORTCUT) {
135
+ return `Comment on ${kind} — press ${key} again while picking to cycle`;
140
136
  }
141
137
  function kindPhrase(kind, human) {
142
138
  if (human === void 0) return "somewhere on this page";
@@ -144,4 +140,4 @@ function kindPhrase(kind, human) {
144
140
  return kind === "region" ? `an area of ${human}` : human;
145
141
  }
146
142
  //#endregion
147
- export { ACCOUNT_COPY, APPROVE_COPY, CORNER_LABELS, CORNER_ORDER, FILTERS_LABEL, FILTER_LABELS, FILTER_ORDER, ISLAND_COPY, PICK_LABELS, PICK_ORDER, SETTINGS_COPY, TALLY_ORDER, THEME_LABELS, THEME_TITLES, UNSENT_COPY, kindPhrase, openLabel, pickTitle, tallyTitle, triggerLabel };
143
+ export { ACCOUNT_COPY, APPROVE_COPY, CORNER_LABELS, CORNER_ORDER, FILTERS_LABEL, FILTER_LABELS, FILTER_ORDER, ISLAND_COPY, PICK_LABELS, SETTINGS_COPY, TALLY_ORDER, THEME_LABELS, THEME_TITLES, UNSENT_COPY, kindPhrase, openLabel, pickTitle, tallyTitle, triggerLabel };
package/dist/maple.js CHANGED
@@ -7,7 +7,6 @@ import { MapleComposer } from "./composer/composer.js";
7
7
  import { MapleDetail } from "./composer/detail.js";
8
8
  import { MapleScoreCard } from "./composer/score.js";
9
9
  import { MapleTarget } from "./composer/target.js";
10
- import { PICK_ORDER } from "./island/language.js";
11
10
  import { Approve } from "./island/approve.js";
12
11
  import { IslandContent } from "./island/content.js";
13
12
  import { Filters } from "./island/filters.js";
@@ -24,6 +23,7 @@ import { MapleMock } from "./mock/mock.js";
24
23
  import { MapleNotice } from "./notice/notice.js";
25
24
  import { MaplePicker } from "./picker/picker.js";
26
25
  import { createElement, forwardRef } from "react";
26
+ import { PICK_ORDER } from "@maple-kit/core/client";
27
27
  //#region src/maple.ts
28
28
  const LOAD_CALLS = [
29
29
  "load",
@@ -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
@@ -232,9 +232,8 @@ function ringCss() {
232
232
  will-change: transform;
233
233
  }
234
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
- }
235
+ /* No quieter hovered ring: pointing at a mark or tabbing onto it is asking
236
+ "which one", and a hairline answered it too faintly to be seen. */
238
237
 
239
238
  /* A passage is its own lines and nothing else: the box around them is the
240
239
  paragraph, and outlining that says the comment is on the paragraph. */
@@ -254,7 +253,9 @@ function ringCss() {
254
253
 
255
254
 
256
255
  /* 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. */
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. */
258
259
  .mk-ring-label {
259
260
  position: absolute;
260
261
  bottom: 100%;
@@ -266,6 +267,7 @@ function ringCss() {
266
267
  padding: 1px 7px 2px;
267
268
  border-radius: var(--mk-r-xs) var(--mk-r-xs) var(--mk-r-xs) 0;
268
269
  background: var(--mk-accent);
270
+ box-shadow: var(--mk-sh2);
269
271
  color: var(--mk-accent-ink);
270
272
  font-size: 10px;
271
273
  font-weight: 600;
@@ -278,6 +280,18 @@ function ringCss() {
278
280
  border-radius: 0 var(--mk-r-xs) var(--mk-r-xs) var(--mk-r-xs);
279
281
  }
280
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
+
281
295
  .mk-ring-name,
282
296
  .mk-ring-note {
283
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 };
@@ -7,7 +7,7 @@ export declare const PICKER_COPY: {
7
7
  readonly cancel: "Cancel";
8
8
  readonly cancelHint: "Esc";
9
9
  readonly bar: "Choosing what to comment on";
10
- readonly cycle: "Press t to cycle";
10
+ readonly cycle: "Press c again to cycle";
11
11
  };
12
12
  /** What a screen reader hears on the bar while a pick is armed. */
13
13
  export declare function pickerLabel(kind: PickKind): string;
@@ -8,7 +8,7 @@ const PICKER_COPY = {
8
8
  cancel: "Cancel",
9
9
  cancelHint: "Esc",
10
10
  bar: "Choosing what to comment on",
11
- cycle: "Press t to cycle"
11
+ cycle: "Press c again to cycle"
12
12
  };
13
13
  function pickerLabel(kind) {
14
14
  return `${PICKER_COPY.bar}: ${PICK_HINTS[kind]}.`;
@@ -1,12 +1,12 @@
1
1
  import { useMapleUi } from "../context.js";
2
2
  import { useShots } from "../shots.js";
3
- import { PICK_ORDER } from "../island/language.js";
4
3
  import { ringLabel } from "../marks/label.js";
5
4
  import { MapleTargetRing } from "../marks/ring.js";
6
5
  import { PICKER_COPY, PICK_HINTS, pickerLabel } from "./language.js";
7
6
  import { startSession } from "./session.js";
8
7
  import { elementOf, targetFor } from "./target.js";
9
- import { Fragment, createElement, useCallback, useEffect, useRef, useState } from "react";
8
+ import { Fragment, createElement, useCallback, useEffect, useState } from "react";
9
+ import { PICK_ORDER } from "@maple-kit/core/client";
10
10
  import { captureContext, watchPickKeys } from "@maple-kit/core/overlay";
11
11
  import { useMaple, useMapleClient } from "@maple-kit/react";
12
12
  import { captureElement } from "@maple-kit/core/screenshot";
@@ -41,7 +41,7 @@ function MaplePicker(props) {
41
41
  pick.armed,
42
42
  root
43
43
  ]);
44
- useKeys(pick.armed, kind, client);
44
+ useKeys(pick.armed, client);
45
45
  if (!pick.armed || !kind) return null;
46
46
  return createElement(Fragment, null, kind === "text" ? null : createElement("div", { className: shieldClass(props, kind) }), band ? createElement("div", {
47
47
  className: "mk-band",
@@ -78,16 +78,13 @@ function capture(element, shots) {
78
78
  function reasonOf(error) {
79
79
  return error instanceof Error ? error.message : String(error);
80
80
  }
81
- function useKeys(armed, kind, client) {
82
- const latest = useRef(kind);
83
- latest.current = kind;
81
+ function useKeys(armed, client) {
84
82
  useEffect(() => {
85
83
  if (!armed) return;
86
84
  const stop = new AbortController();
87
85
  watchPickKeys({
88
86
  signal: stop.signal,
89
- onCancel: () => client.disarm(),
90
- onCycle: () => client.arm(next(latest.current))
87
+ onCancel: () => client.disarm()
91
88
  });
92
89
  return () => stop.abort();
93
90
  }, [armed, client]);
@@ -97,10 +94,6 @@ function onOwnControl(event, root) {
97
94
  if (!(target instanceof Element) || target.getRootNode() !== root) return false;
98
95
  return !target.classList.contains("mk-shield");
99
96
  }
100
- function next(kind) {
101
- const at = kind === void 0 ? -1 : PICK_ORDER.indexOf(kind);
102
- return PICK_ORDER[(at + 1) % PICK_ORDER.length] ?? "element";
103
- }
104
97
  function Hint(props) {
105
98
  const client = useMapleClient();
106
99
  return createElement("div", {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@maple-kit/ui",
3
- "version": "0.10.0",
3
+ "version": "0.12.0",
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.10.0",
37
- "@maple-kit/mock": "0.10.0",
38
- "@maple-kit/react": "0.10.0"
36
+ "@maple-kit/core": "0.12.0",
37
+ "@maple-kit/mock": "0.12.0",
38
+ "@maple-kit/react": "0.12.0"
39
39
  },
40
40
  "peerDependencies": {
41
41
  "react": "^18.2.0 || ^19.0.0",