@maple-kit/ui 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (173) hide show
  1. package/LICENSE +202 -0
  2. package/NOTICE +9 -0
  3. package/README.md +39 -0
  4. package/dist/composer/actions.d.ts +15 -0
  5. package/dist/composer/actions.js +59 -0
  6. package/dist/composer/attachments.d.ts +33 -0
  7. package/dist/composer/attachments.js +166 -0
  8. package/dist/composer/badge.d.ts +13 -0
  9. package/dist/composer/badge.js +25 -0
  10. package/dist/composer/body.d.ts +13 -0
  11. package/dist/composer/body.js +119 -0
  12. package/dist/composer/composer.d.ts +23 -0
  13. package/dist/composer/composer.js +122 -0
  14. package/dist/composer/css.js +559 -0
  15. package/dist/composer/detail.d.ts +32 -0
  16. package/dist/composer/detail.js +102 -0
  17. package/dist/composer/emoji.d.ts +54 -0
  18. package/dist/composer/emoji.js +131 -0
  19. package/dist/composer/index.d.ts +15 -0
  20. package/dist/composer/index.js +15 -0
  21. package/dist/composer/inset.js +20 -0
  22. package/dist/composer/leave.d.ts +19 -0
  23. package/dist/composer/leave.js +34 -0
  24. package/dist/composer/peek.d.ts +21 -0
  25. package/dist/composer/peek.js +29 -0
  26. package/dist/composer/phrase.d.ts +27 -0
  27. package/dist/composer/phrase.js +30 -0
  28. package/dist/composer/picker-emoji.d.ts +34 -0
  29. package/dist/composer/picker-emoji.js +48 -0
  30. package/dist/composer/prompt.js +31 -0
  31. package/dist/composer/scope.d.ts +11 -0
  32. package/dist/composer/scope.js +16 -0
  33. package/dist/composer/target.d.ts +13 -0
  34. package/dist/composer/target.js +45 -0
  35. package/dist/context.d.ts +21 -0
  36. package/dist/context.js +16 -0
  37. package/dist/data.d.ts +52 -0
  38. package/dist/data.js +30 -0
  39. package/dist/icons/branch.d.ts +4 -0
  40. package/dist/icons/branch.js +8 -0
  41. package/dist/icons/cog.d.ts +4 -0
  42. package/dist/icons/cog.js +8 -0
  43. package/dist/icons/commit.d.ts +4 -0
  44. package/dist/icons/commit.js +5 -0
  45. package/dist/icons/crossfade.d.ts +15 -0
  46. package/dist/icons/crossfade.js +27 -0
  47. package/dist/icons/element.d.ts +4 -0
  48. package/dist/icons/element.js +5 -0
  49. package/dist/icons/icon.d.ts +24 -0
  50. package/dist/icons/icon.js +27 -0
  51. package/dist/icons/index.d.ts +13 -0
  52. package/dist/icons/index.js +13 -0
  53. package/dist/icons/noplace.d.ts +4 -0
  54. package/dist/icons/noplace.js +5 -0
  55. package/dist/icons/region.d.ts +4 -0
  56. package/dist/icons/region.js +8 -0
  57. package/dist/icons/smiley.d.ts +4 -0
  58. package/dist/icons/smiley.js +8 -0
  59. package/dist/icons/sparkle.d.ts +4 -0
  60. package/dist/icons/sparkle.js +8 -0
  61. package/dist/icons/target.d.ts +4 -0
  62. package/dist/icons/target.js +5 -0
  63. package/dist/icons/text.d.ts +4 -0
  64. package/dist/icons/text.js +5 -0
  65. package/dist/index.d.ts +11 -0
  66. package/dist/index.js +11 -0
  67. package/dist/island/account.d.ts +10 -0
  68. package/dist/island/account.js +52 -0
  69. package/dist/island/comments.d.ts +24 -0
  70. package/dist/island/comments.js +40 -0
  71. package/dist/island/content.d.ts +10 -0
  72. package/dist/island/content.js +26 -0
  73. package/dist/island/context.d.ts +58 -0
  74. package/dist/island/context.js +23 -0
  75. package/dist/island/css.d.ts +13 -0
  76. package/dist/island/css.js +1020 -0
  77. package/dist/island/drag.d.ts +10 -0
  78. package/dist/island/drag.js +77 -0
  79. package/dist/island/filters.d.ts +10 -0
  80. package/dist/island/filters.js +63 -0
  81. package/dist/island/header.d.ts +32 -0
  82. package/dist/island/header.js +47 -0
  83. package/dist/island/index.d.ts +18 -0
  84. package/dist/island/index.js +18 -0
  85. package/dist/island/island.d.ts +12 -0
  86. package/dist/island/island.js +111 -0
  87. package/dist/island/item.d.ts +12 -0
  88. package/dist/island/item.js +158 -0
  89. package/dist/island/language.d.ts +55 -0
  90. package/dist/island/language.js +125 -0
  91. package/dist/island/leaf.js +17 -0
  92. package/dist/island/list.d.ts +12 -0
  93. package/dist/island/list.js +31 -0
  94. package/dist/island/new-comment.d.ts +18 -0
  95. package/dist/island/new-comment.js +56 -0
  96. package/dist/island/settings.d.ts +10 -0
  97. package/dist/island/settings.js +134 -0
  98. package/dist/island/stagger.d.ts +12 -0
  99. package/dist/island/stagger.js +4 -0
  100. package/dist/island/time.d.ts +17 -0
  101. package/dist/island/time.js +29 -0
  102. package/dist/island/trigger.d.ts +10 -0
  103. package/dist/island/trigger.js +39 -0
  104. package/dist/language.d.ts +45 -0
  105. package/dist/language.js +66 -0
  106. package/dist/maple.d.ts +25 -0
  107. package/dist/maple.js +65 -0
  108. package/dist/marks/avatar.d.ts +16 -0
  109. package/dist/marks/avatar.js +36 -0
  110. package/dist/marks/css.d.ts +12 -0
  111. package/dist/marks/css.js +371 -0
  112. package/dist/marks/frame.d.ts +25 -0
  113. package/dist/marks/frame.js +53 -0
  114. package/dist/marks/geometry.d.ts +55 -0
  115. package/dist/marks/geometry.js +61 -0
  116. package/dist/marks/index.d.ts +14 -0
  117. package/dist/marks/index.js +14 -0
  118. package/dist/marks/label.d.ts +23 -0
  119. package/dist/marks/label.js +31 -0
  120. package/dist/marks/layer.d.ts +16 -0
  121. package/dist/marks/layer.js +194 -0
  122. package/dist/marks/leaf.d.ts +18 -0
  123. package/dist/marks/leaf.js +7 -0
  124. package/dist/marks/mark.d.ts +28 -0
  125. package/dist/marks/mark.js +43 -0
  126. package/dist/marks/nudge.d.ts +50 -0
  127. package/dist/marks/nudge.js +76 -0
  128. package/dist/marks/paint.d.ts +11 -0
  129. package/dist/marks/paint.js +19 -0
  130. package/dist/marks/placement.d.ts +24 -0
  131. package/dist/marks/placement.js +28 -0
  132. package/dist/marks/ring.d.ts +28 -0
  133. package/dist/marks/ring.js +85 -0
  134. package/dist/marks/shape.d.ts +21 -0
  135. package/dist/marks/shape.js +51 -0
  136. package/dist/notice/css.d.ts +11 -0
  137. package/dist/notice/css.js +74 -0
  138. package/dist/notice/index.d.ts +4 -0
  139. package/dist/notice/index.js +4 -0
  140. package/dist/notice/language.d.ts +15 -0
  141. package/dist/notice/language.js +14 -0
  142. package/dist/notice/notice.d.ts +14 -0
  143. package/dist/notice/notice.js +49 -0
  144. package/dist/part.d.ts +9 -0
  145. package/dist/part.js +11 -0
  146. package/dist/picker/css.d.ts +13 -0
  147. package/dist/picker/css.js +141 -0
  148. package/dist/picker/index.d.ts +6 -0
  149. package/dist/picker/index.js +6 -0
  150. package/dist/picker/language.d.ts +14 -0
  151. package/dist/picker/language.js +17 -0
  152. package/dist/picker/picker.d.ts +18 -0
  153. package/dist/picker/picker.js +164 -0
  154. package/dist/picker/session.d.ts +16 -0
  155. package/dist/picker/session.js +35 -0
  156. package/dist/picker/target.d.ts +23 -0
  157. package/dist/picker/target.js +37 -0
  158. package/dist/root.d.ts +42 -0
  159. package/dist/root.js +75 -0
  160. package/dist/shots.js +32 -0
  161. package/dist/slot.d.ts +28 -0
  162. package/dist/slot.js +52 -0
  163. package/dist/slots.d.ts +24 -0
  164. package/dist/slots.js +75 -0
  165. package/dist/stylesheet.d.ts +15 -0
  166. package/dist/stylesheet.js +188 -0
  167. package/dist/theme.d.ts +20 -0
  168. package/dist/theme.js +13 -0
  169. package/dist/tip.d.ts +40 -0
  170. package/dist/tip.js +82 -0
  171. package/dist/tokens.d.ts +28 -0
  172. package/dist/tokens.js +180 -0
  173. package/package.json +56 -0
@@ -0,0 +1,54 @@
1
+ //#region src/composer/emoji.d.ts
2
+ /**
3
+ * The small set a review comment actually uses, and the `:` that finds it.
4
+ *
5
+ * Not a picker over every emoji there is: a reviewer reaching for one is
6
+ * softening a criticism or flagging a blocker, and a search over 3,600 glyphs
7
+ * is a worse way to do either than twelve buttons. Not a rich-text editor
8
+ * either — the body is a plain string on the wire, so Tiptap would buy a
9
+ * document model nothing here uses, and a textarea knows its own caret.
10
+ */
11
+ /** One glyph and the words that find it. */
12
+ export interface EmojiChoice {
13
+ readonly glyph: string;
14
+ /** The first is the shortcode shown; the rest are aliases that also match. */
15
+ readonly names: readonly string[];
16
+ }
17
+ /**
18
+ * Twelve, chosen for a review: agreement, doubt, a blocker, a nit, a thing
19
+ * that made someone laugh, and the three that stand in for "ship it".
20
+ */
21
+ export declare const REVIEW_EMOJI: readonly EmojiChoice[];
22
+ /** How many the shortcode menu offers at once. More is a list, not a hint. */
23
+ export declare const SHORTCODE_LIMIT = 6;
24
+ /** A `:word` being typed, and where in the body it starts. */
25
+ export interface Shortcode {
26
+ readonly query: string;
27
+ /** Index of the colon itself, so the replacement knows what to cut. */
28
+ readonly start: number;
29
+ }
30
+ /**
31
+ * The shortcode the caret is sitting at the end of, if any. It has to follow
32
+ * a space or the start of the body, or every URL with a colon in it opens a
33
+ * menu halfway through being typed.
34
+ */
35
+ export declare function shortcodeAt(body: string, caret: number): Shortcode | undefined;
36
+ /** The emoji a shortcode names, best prefix first, at most six. */
37
+ export declare function searchEmoji(query: string): readonly EmojiChoice[];
38
+ /** A body with `glyph` written over `[start, end)`, and where the caret lands. */
39
+ export interface Written {
40
+ readonly body: string;
41
+ readonly caret: number;
42
+ }
43
+ /**
44
+ * Writes the glyph over `[start, end)`, spaced on both sides. A glyph jammed
45
+ * against the word before it is a typo, and one with nothing after it leaves
46
+ * nowhere to keep typing.
47
+ */
48
+ export declare function writeAt(body: string, start: number, end: number, glyph: string): Written;
49
+ /**
50
+ * Appended, not inserted at the caret: used where the caller has no field to
51
+ * ask, which is any composition that draws the control outside the body.
52
+ */
53
+ export declare function appended(body: string, glyph: string): string;
54
+ //#endregion
@@ -0,0 +1,131 @@
1
+ //#region src/composer/emoji.ts
2
+ const REVIEW_EMOJI = [
3
+ {
4
+ glyph: "👍",
5
+ names: [
6
+ "+1",
7
+ "yes",
8
+ "agree",
9
+ "thumbsup"
10
+ ]
11
+ },
12
+ {
13
+ glyph: "🎉",
14
+ names: [
15
+ "tada",
16
+ "nice",
17
+ "party"
18
+ ]
19
+ },
20
+ {
21
+ glyph: "🙏",
22
+ names: [
23
+ "please",
24
+ "thanks",
25
+ "pray"
26
+ ]
27
+ },
28
+ {
29
+ glyph: "🤔",
30
+ names: [
31
+ "thinking",
32
+ "hmm",
33
+ "unsure"
34
+ ]
35
+ },
36
+ {
37
+ glyph: "😅",
38
+ names: [
39
+ "sweat",
40
+ "oops",
41
+ "awkward"
42
+ ]
43
+ },
44
+ {
45
+ glyph: "🙈",
46
+ names: [
47
+ "hide",
48
+ "yikes",
49
+ "monkey"
50
+ ]
51
+ },
52
+ {
53
+ glyph: "🐛",
54
+ names: ["bug", "broken"]
55
+ },
56
+ {
57
+ glyph: "🔥",
58
+ names: [
59
+ "fire",
60
+ "hot",
61
+ "great"
62
+ ]
63
+ },
64
+ {
65
+ glyph: "⚠️",
66
+ names: [
67
+ "warning",
68
+ "careful",
69
+ "risk"
70
+ ]
71
+ },
72
+ {
73
+ glyph: "🚀",
74
+ names: [
75
+ "ship",
76
+ "rocket",
77
+ "launch"
78
+ ]
79
+ },
80
+ {
81
+ glyph: "✨",
82
+ names: [
83
+ "sparkles",
84
+ "polish",
85
+ "neat"
86
+ ]
87
+ },
88
+ {
89
+ glyph: "💡",
90
+ names: [
91
+ "idea",
92
+ "suggestion",
93
+ "tip"
94
+ ]
95
+ }
96
+ ];
97
+ const SHORTCODE_LIMIT = 6;
98
+ const COLON_WORD = /(?:^|\s):([a-z\d+_-]*)$/i;
99
+ function shortcodeAt(body, caret) {
100
+ const before = body.slice(0, caret);
101
+ const found = COLON_WORD.exec(before);
102
+ if (!found) return void 0;
103
+ const query = found[1] ?? "";
104
+ return {
105
+ query,
106
+ start: caret - query.length - 1
107
+ };
108
+ }
109
+ function searchEmoji(query) {
110
+ const needle = query.toLowerCase();
111
+ if (needle === "") return REVIEW_EMOJI.slice(0, 6);
112
+ const starts = REVIEW_EMOJI.filter((one) => one.names.some((name) => name.startsWith(needle)));
113
+ const rest = REVIEW_EMOJI.filter((one) => !starts.includes(one) && one.names.some((name) => name.includes(needle)));
114
+ return [...starts, ...rest].slice(0, 6);
115
+ }
116
+ function writeAt(body, start, end, glyph) {
117
+ const head = body.slice(0, start);
118
+ const tail = body.slice(end);
119
+ const before = head === "" || /\s$/.test(head) ? "" : " ";
120
+ const after = tail.startsWith(" ") ? "" : " ";
121
+ return {
122
+ body: `${head}${before}${glyph}${after}${tail}`,
123
+ caret: start + before.length + glyph.length + after.length
124
+ };
125
+ }
126
+ function appended(body, glyph) {
127
+ if (body === "") return glyph;
128
+ return body.endsWith(" ") ? `${body}${glyph}` : `${body} ${glyph}`;
129
+ }
130
+ //#endregion
131
+ export { REVIEW_EMOJI, SHORTCODE_LIMIT, appended, searchEmoji, shortcodeAt, writeAt };
@@ -0,0 +1,15 @@
1
+ import { SHEET_BREAKPOINT_PX } from "../tokens.js";
2
+ import { CANCEL_LABEL, MapleActions, MapleActionsProps, SEND_LABEL } from "./actions.js";
3
+ import { ATTACH_WORDS, MapleAttachments, MapleAttachmentsProps } from "./attachments.js";
4
+ import { MapleContextBadge, MapleContextProps } from "./badge.js";
5
+ import { COMPOSER_PLACEHOLDER, MapleBody, MapleBodyProps } from "./body.js";
6
+ import { LeaveAsk, createLeaveAsk } from "./leave.js";
7
+ import { ComposerScopeError, SheetDetent } from "./scope.js";
8
+ import { COMPOSER_LABEL, DETENT_LABELS, MapleComposer, MapleComposerProps } from "./composer.js";
9
+ import { MapleDetail, MapleDetailProps, detailRows } from "./detail.js";
10
+ import { EmojiChoice, REVIEW_EMOJI, SHORTCODE_LIMIT, Shortcode, Written, appended, searchEmoji, shortcodeAt, writeAt } from "./emoji.js";
11
+ import { peeks } from "./peek.js";
12
+ import { KIND_WORDS, LEAVE_DISCARD, LEAVE_KEEP, QUOTE_LIMIT, TARGET_PREFIX, UNNAMED_TARGET, leaveMessage, quotedText, targetName, targetPhrase } from "./phrase.js";
13
+ import { EMOJI_COPY, EMOJI_ICON_PX, EmojiGrid, EmojiGridProps, MapleEmoji, MapleEmojiProps } from "./picker-emoji.js";
14
+ import { CLOSE_LABEL, DETAIL_ATTRIBUTE, MapleTarget, MapleTargetProps } from "./target.js";
15
+ export { ATTACH_WORDS, MapleActions as Actions, MapleAttachments as Attachments, MapleBody as Body, CANCEL_LABEL, CLOSE_LABEL, COMPOSER_LABEL, COMPOSER_PLACEHOLDER, 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_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, MapleTarget, type MapleTargetProps, QUOTE_LIMIT, REVIEW_EMOJI, SEND_LABEL, SHEET_BREAKPOINT_PX, SHORTCODE_LIMIT, type SheetDetent, type Shortcode, TARGET_PREFIX, MapleTarget as Target, UNNAMED_TARGET, type Written, appended, createLeaveAsk, detailRows, leaveMessage, peeks, quotedText, searchEmoji, shortcodeAt, targetName, targetPhrase, writeAt };
@@ -0,0 +1,15 @@
1
+ import { SHEET_BREAKPOINT_PX } from "../tokens.js";
2
+ import { ComposerScopeError } from "./scope.js";
3
+ import { CANCEL_LABEL, MapleActions, SEND_LABEL } from "./actions.js";
4
+ import { ATTACH_WORDS, MapleAttachments } from "./attachments.js";
5
+ import { MapleContextBadge } from "./badge.js";
6
+ import { REVIEW_EMOJI, SHORTCODE_LIMIT, appended, searchEmoji, shortcodeAt, writeAt } from "./emoji.js";
7
+ import { EMOJI_COPY, EMOJI_ICON_PX, EmojiGrid, MapleEmoji } from "./picker-emoji.js";
8
+ import { COMPOSER_PLACEHOLDER, MapleBody } from "./body.js";
9
+ import { peeks } from "./peek.js";
10
+ import { KIND_WORDS, LEAVE_DISCARD, LEAVE_KEEP, QUOTE_LIMIT, TARGET_PREFIX, UNNAMED_TARGET, leaveMessage, quotedText, targetName, targetPhrase } from "./phrase.js";
11
+ import { COMPOSER_LABEL, DETENT_LABELS, MapleComposer } from "./composer.js";
12
+ import { MapleDetail, detailRows } from "./detail.js";
13
+ import { createLeaveAsk } from "./leave.js";
14
+ import { CLOSE_LABEL, DETAIL_ATTRIBUTE, MapleTarget } from "./target.js";
15
+ export { ATTACH_WORDS, MapleActions as Actions, MapleAttachments as Attachments, MapleBody as Body, CANCEL_LABEL, CLOSE_LABEL, COMPOSER_LABEL, COMPOSER_PLACEHOLDER, MapleComposer as Composer, ComposerScopeError, MapleContextBadge as Context, DETAIL_ATTRIBUTE, DETENT_LABELS, MapleDetail as Detail, EMOJI_COPY, EMOJI_ICON_PX, MapleEmoji as Emoji, EmojiGrid, KIND_WORDS, LEAVE_DISCARD, LEAVE_KEEP, MapleActions, MapleAttachments, MapleBody, MapleComposer, MapleContextBadge, MapleDetail, MapleEmoji, MapleTarget, QUOTE_LIMIT, REVIEW_EMOJI, SEND_LABEL, SHEET_BREAKPOINT_PX, SHORTCODE_LIMIT, TARGET_PREFIX, MapleTarget as Target, UNNAMED_TARGET, appended, createLeaveAsk, detailRows, leaveMessage, peeks, quotedText, searchEmoji, shortcodeAt, targetName, targetPhrase, writeAt };
@@ -0,0 +1,20 @@
1
+ import "../tokens.js";
2
+ import { useEffect } from "react";
3
+ //#region src/composer/inset.ts
4
+ const PADDING = "padding-inline-end";
5
+ function useInset(frame, panel, view) {
6
+ useEffect(() => {
7
+ if (!frame || !panel || !view) return;
8
+ if (!view.matchMedia(`(min-width: 640px)`).matches) return;
9
+ frame.style.setProperty(PADDING, `${panel.getBoundingClientRect().width}px`);
10
+ return () => {
11
+ frame.style.removeProperty(PADDING);
12
+ };
13
+ }, [
14
+ frame,
15
+ panel,
16
+ view
17
+ ]);
18
+ }
19
+ //#endregion
20
+ export { useInset };
@@ -0,0 +1,19 @@
1
+ import { LeaveAnswer, LeaveQuestion } from "@maple-kit/core/client";
2
+ //#region src/composer/leave.d.ts
3
+ /** The prompt, from both sides: the controller asks, the composer answers. */
4
+ export interface LeaveAsk {
5
+ /** Pass to `MapleClientOptions.askToLeave`. Resolves when the reviewer picks. */
6
+ readonly askToLeave: (question: LeaveQuestion) => Promise<LeaveAnswer>;
7
+ /** For `useSyncExternalStore`; returns the unsubscribe. */
8
+ readonly subscribe: (listener: () => void) => () => void;
9
+ /** The question on screen, or undefined when nothing is being asked. */
10
+ readonly pending: () => LeaveQuestion | undefined;
11
+ /** Answers the question on screen. A second call does nothing. */
12
+ readonly answer: (answer: LeaveAnswer) => void;
13
+ }
14
+ /**
15
+ * Builds the prompt's store. Nothing here touches the DOM or React, so the
16
+ * copy and the answering are testable without rendering anything.
17
+ */
18
+ export declare function createLeaveAsk(): LeaveAsk;
19
+ //#endregion
@@ -0,0 +1,34 @@
1
+ //#region src/composer/leave.ts
2
+ function createLeaveAsk() {
3
+ const listeners = /* @__PURE__ */ new Set();
4
+ let asked;
5
+ const announce = () => {
6
+ for (const listener of listeners) listener();
7
+ };
8
+ return {
9
+ askToLeave: (question) => {
10
+ if (asked) return Promise.resolve("keep");
11
+ return new Promise((resolve) => {
12
+ asked = {
13
+ question,
14
+ settle: resolve
15
+ };
16
+ announce();
17
+ });
18
+ },
19
+ subscribe: (listener) => {
20
+ listeners.add(listener);
21
+ return () => listeners.delete(listener);
22
+ },
23
+ pending: () => asked?.question,
24
+ answer: (answer) => {
25
+ const current = asked;
26
+ if (!current) return;
27
+ asked = void 0;
28
+ announce();
29
+ current.settle(answer);
30
+ }
31
+ };
32
+ }
33
+ //#endregion
34
+ export { createLeaveAsk };
@@ -0,0 +1,21 @@
1
+ //#region src/composer/peek.d.ts
2
+ /**
3
+ * Hold Space to see the layout the comment is about.
4
+ *
5
+ * Where Maple is mounted inside someone else's application it cannot move the
6
+ * layout, so the panel gets out of the way instead. A bare key checks its
7
+ * modifiers and where it landed for the same reason `c` does: Space in a
8
+ * textarea is a space.
9
+ */
10
+ interface Held {
11
+ readonly key: string;
12
+ readonly code?: string;
13
+ readonly metaKey?: boolean;
14
+ readonly ctrlKey?: boolean;
15
+ readonly altKey?: boolean;
16
+ readonly shiftKey?: boolean;
17
+ readonly target?: unknown;
18
+ }
19
+ /** True while Space means "show me the page" and not "type a space". */
20
+ export declare function peeks(event: Held): boolean;
21
+ //#endregion
@@ -0,0 +1,29 @@
1
+ import { useEffect, useState } from "react";
2
+ import { isEditable } from "@maple-kit/core/client";
3
+ //#region src/composer/peek.ts
4
+ const PEEK_CODE = "Space";
5
+ function peeks(event) {
6
+ if (event.code !== PEEK_CODE && event.key !== " ") return false;
7
+ if (event.metaKey || event.ctrlKey || event.altKey || event.shiftKey) return false;
8
+ return !isEditable(event.target);
9
+ }
10
+ function useHoldToPeek(view, enabled) {
11
+ const [peeking, setPeeking] = useState(false);
12
+ useEffect(() => {
13
+ if (!view || !enabled) return;
14
+ const controller = new AbortController();
15
+ const options = { signal: controller.signal };
16
+ const down = (event) => {
17
+ if (!peeks(event)) return;
18
+ event.preventDefault();
19
+ setPeeking(true);
20
+ };
21
+ view.addEventListener("keydown", down, options);
22
+ view.addEventListener("keyup", () => setPeeking(false), options);
23
+ view.addEventListener("blur", () => setPeeking(false), options);
24
+ return () => controller.abort();
25
+ }, [view, enabled]);
26
+ return enabled && peeking;
27
+ }
28
+ //#endregion
29
+ export { peeks, useHoldToPeek };
@@ -0,0 +1,27 @@
1
+ import { ComposerTarget, LeaveSubject, PickKind } from "@maple-kit/core/client";
2
+ //#region src/composer/phrase.d.ts
3
+ /** The word on the kind chip: a text pick is a passage to everyone but the code. */
4
+ export declare const KIND_WORDS: Readonly<Record<PickKind, string>>;
5
+ /** What a pick is called when nothing on the page names it. */
6
+ export declare const UNNAMED_TARGET = "this page";
7
+ /** The preposition the phrase is rendered behind. */
8
+ export declare const TARGET_PREFIX = "on ";
9
+ /** The label the controller resolved, the one `labelFor` reads, or the page. */
10
+ export declare function targetName(target: ComposerTarget): string;
11
+ /** Longer than this and the quote is trimmed, because the field is 336px. */
12
+ export declare const QUOTE_LIMIT = 64;
13
+ /** After "on": an element is its own name; a passage and a region are held. */
14
+ export declare function targetPhrase(kind: PickKind, name: string): string;
15
+ /**
16
+ * The words a passage pick actually selected, trimmed to fit the header. A
17
+ * reviewer who selected six words has to read those six words back, or the
18
+ * composer is asking them to remember what they highlighted.
19
+ */
20
+ export declare function quotedText(target: ComposerTarget): string | undefined;
21
+ /** What is about to be lost, named. "Are you sure?" answers nothing. */
22
+ export declare function leaveMessage(subject: LeaveSubject): string;
23
+ /** The safe answer, and the one focused when the prompt opens. */
24
+ export declare const LEAVE_KEEP = "Keep writing";
25
+ /** The only way a draft is thrown away on the way out. */
26
+ export declare const LEAVE_DISCARD = "Discard";
27
+ //#endregion
@@ -0,0 +1,30 @@
1
+ import { labelFor } from "@maple-kit/core/anchor";
2
+ //#region src/composer/phrase.ts
3
+ const KIND_WORDS = {
4
+ element: "element",
5
+ region: "region",
6
+ text: "passage"
7
+ };
8
+ const UNNAMED_TARGET = "this page";
9
+ const TARGET_PREFIX = "on ";
10
+ function targetName(target) {
11
+ return target.label ?? labelFor({ anchor: target.anchor }) ?? "this page";
12
+ }
13
+ const QUOTE_LIMIT = 64;
14
+ function targetPhrase(kind, name) {
15
+ if (kind === "text") return `a passage in ${name}`;
16
+ return kind === "region" ? `an area of ${name}` : name;
17
+ }
18
+ function quotedText(target) {
19
+ if (target.kind !== "text") return void 0;
20
+ const exact = target.anchor.quote?.exact?.trim();
21
+ if (!exact) return void 0;
22
+ return exact.length > 64 ? `${exact.slice(0, 64).trimEnd()}…` : exact;
23
+ }
24
+ function leaveMessage(subject) {
25
+ return `You have an unsent comment on ${subject.label ?? "this page"}.`;
26
+ }
27
+ const LEAVE_KEEP = "Keep writing";
28
+ const LEAVE_DISCARD = "Discard";
29
+ //#endregion
30
+ export { KIND_WORDS, LEAVE_DISCARD, LEAVE_KEEP, QUOTE_LIMIT, TARGET_PREFIX, UNNAMED_TARGET, leaveMessage, quotedText, targetName, targetPhrase };
@@ -0,0 +1,34 @@
1
+ import { AsChildProps } from "../slot.js";
2
+ import { EmojiChoice } from "./emoji.js";
3
+ import { ReactElement } from "react";
4
+ //#region src/composer/picker-emoji.d.ts
5
+ /** The control. Takes `asChild` and nothing that changes its look. */
6
+ export interface MapleEmojiProps extends AsChildProps {
7
+ readonly className?: string;
8
+ readonly open?: boolean;
9
+ readonly onToggle?: (open: boolean) => void;
10
+ /** Called with the glyph. The caller owns the caret, so it does the writing. */
11
+ readonly onPick?: (glyph: string) => void;
12
+ /** Replaces the default set, in the order they are shown. */
13
+ readonly emoji?: readonly EmojiChoice[];
14
+ }
15
+ /** Bigger than the 13px the icons default to: it is a control, not a footnote. */
16
+ export declare const EMOJI_ICON_PX = 16;
17
+ /** Every word this part shows. */
18
+ export declare const EMOJI_COPY: {
19
+ readonly open: "Add an emoji";
20
+ readonly close: "Close the emoji list";
21
+ readonly hint: "Type : to search them";
22
+ };
23
+ /** The button, and the grid it opens above itself. */
24
+ export declare const MapleEmoji: import("react").ForwardRefExoticComponent<MapleEmojiProps & import("react").RefAttributes<HTMLButtonElement>>;
25
+ /** What both surfaces draw: a row of glyphs, each its own button. */
26
+ export interface EmojiGridProps {
27
+ readonly emoji: readonly EmojiChoice[];
28
+ readonly onPick: (glyph: string) => void;
29
+ readonly active?: number;
30
+ readonly className?: string;
31
+ }
32
+ /** The grid. Labelled by shortcode, so a screen reader hears a word. */
33
+ export declare function EmojiGrid(props: EmojiGridProps): ReactElement;
34
+ //#endregion
@@ -0,0 +1,48 @@
1
+ import { Slot } from "../slot.js";
2
+ import { REVIEW_EMOJI } from "./emoji.js";
3
+ import { SmileyIcon } from "../icons/smiley.js";
4
+ import { createElement, forwardRef } from "react";
5
+ //#region src/composer/picker-emoji.ts
6
+ const EMOJI_ICON_PX = 16;
7
+ const EMOJI_COPY = {
8
+ open: "Add an emoji",
9
+ close: "Close the emoji list",
10
+ hint: "Type : to search them"
11
+ };
12
+ const MapleEmoji = forwardRef(function MapleEmoji(props, ref) {
13
+ const open = props.open ?? false;
14
+ const Element = props.asChild ? Slot : "button";
15
+ const button = createElement(Element, {
16
+ ref,
17
+ type: "button",
18
+ className: ["mk-emoji-open", props.className].filter(Boolean).join(" "),
19
+ "aria-expanded": open,
20
+ "aria-label": open ? EMOJI_COPY.close : EMOJI_COPY.open,
21
+ title: `${EMOJI_COPY.open} — ${EMOJI_COPY.hint}`,
22
+ onClick: () => props.onToggle?.(!open)
23
+ }, createElement(SmileyIcon, { size: 16 }));
24
+ const grid = createElement(EmojiGrid, {
25
+ emoji: props.emoji ?? REVIEW_EMOJI,
26
+ onPick: (glyph) => props.onPick?.(glyph)
27
+ });
28
+ return createElement("span", { className: "mk-emoji" }, button, open ? grid : null);
29
+ });
30
+ function EmojiGrid(props) {
31
+ return createElement("span", {
32
+ className: ["mk-emoji-grid", props.className].filter(Boolean).join(" "),
33
+ role: "listbox",
34
+ "aria-label": EMOJI_COPY.open
35
+ }, ...props.emoji.map((one, index) => createElement("button", {
36
+ key: one.glyph,
37
+ type: "button",
38
+ role: "option",
39
+ "aria-selected": index === props.active,
40
+ "aria-label": one.names[0] ?? one.glyph,
41
+ title: `:${one.names[0] ?? ""}`,
42
+ className: "mk-emoji-one",
43
+ onMouseDown: (event) => event.preventDefault(),
44
+ onClick: () => props.onPick(one.glyph)
45
+ }, one.glyph)));
46
+ }
47
+ //#endregion
48
+ export { EMOJI_COPY, EMOJI_ICON_PX, EmojiGrid, MapleEmoji };
@@ -0,0 +1,31 @@
1
+ import { LEAVE_DISCARD, LEAVE_KEEP, leaveMessage } from "./phrase.js";
2
+ import { createElement, useEffect, useRef, useState, useSyncExternalStore } from "react";
3
+ //#region src/composer/prompt.ts
4
+ function LeavePrompt(props) {
5
+ const { ask } = props;
6
+ const question = useSyncExternalStore(ask.subscribe, ask.pending, ask.pending);
7
+ const [shown, setShown] = useState(question);
8
+ const keep = useRef(null);
9
+ if (question !== void 0 && question !== shown) setShown(question);
10
+ useEffect(() => {
11
+ if (question) keep.current?.focus();
12
+ }, [question]);
13
+ if (!shown) return null;
14
+ return createElement("div", {
15
+ className: "mk-leave",
16
+ "data-mk-open": String(question !== void 0),
17
+ role: "alertdialog",
18
+ "aria-label": leaveMessage(shown.subject)
19
+ }, createElement("p", { className: "mk-leave-say mk-body" }, leaveMessage(shown.subject)), createElement("div", { className: "mk-leave-ask" }, createElement("button", {
20
+ type: "button",
21
+ className: "mk-btn mk-btn-quiet mk-press",
22
+ onClick: () => ask.answer("discard")
23
+ }, LEAVE_DISCARD), createElement("button", {
24
+ type: "button",
25
+ ref: keep,
26
+ className: "mk-btn mk-btn-primary mk-press",
27
+ onClick: () => ask.answer("keep")
28
+ }, LEAVE_KEEP)));
29
+ }
30
+ //#endregion
31
+ export { LeavePrompt };
@@ -0,0 +1,11 @@
1
+ import { PastedImage } from "@maple-kit/core/screenshot";
2
+ import "@maple-kit/core";
3
+ //#region src/composer/scope.d.ts
4
+ /** How much of the sheet is showing. Two, and no third. */
5
+ export type SheetDetent = "full" | "half";
6
+ /** Thrown by a composer part rendered outside `<Maple.Composer>`. */
7
+ export declare class ComposerScopeError extends Error {
8
+ readonly name = "ComposerScopeError";
9
+ constructor(part: string);
10
+ }
11
+ //#endregion
@@ -0,0 +1,16 @@
1
+ import { createContext, useContext } from "react";
2
+ //#region src/composer/scope.ts
3
+ const ComposerScope = createContext(null);
4
+ var ComposerScopeError = class extends Error {
5
+ name = "ComposerScopeError";
6
+ constructor(part) {
7
+ super(`${part} was rendered outside <Maple.Composer>. Every part needs one above it.`);
8
+ }
9
+ };
10
+ function useComposerScope(part) {
11
+ const value = useContext(ComposerScope);
12
+ if (value === null) throw new ComposerScopeError(part);
13
+ return value;
14
+ }
15
+ //#endregion
16
+ export { ComposerScope, ComposerScopeError, useComposerScope };
@@ -0,0 +1,13 @@
1
+ import { AsChildProps } from "../slot.js";
2
+ //#region src/composer/target.d.ts
3
+ /** The header. Takes `asChild` and nothing that changes its look. */
4
+ export interface MapleTargetProps extends AsChildProps {
5
+ readonly className?: string;
6
+ }
7
+ /** Closing leaves the draft where it is. */
8
+ export declare const CLOSE_LABEL = "Close the composer";
9
+ /** Where developer detail lands next. Default until then. */
10
+ export declare const DETAIL_ATTRIBUTE = "data-mk-detail";
11
+ /** Names the target and carries the close control. */
12
+ export declare const MapleTarget: import("react").ForwardRefExoticComponent<MapleTargetProps & import("react").RefAttributes<HTMLElement>>;
13
+ //#endregion
@@ -0,0 +1,45 @@
1
+ import { Slot } from "../slot.js";
2
+ import { KIND_WORDS, quotedText, targetName, targetPhrase } from "./phrase.js";
3
+ import { IconCrossfade } from "../icons/crossfade.js";
4
+ import { ElementIcon } from "../icons/element.js";
5
+ import { RegionIcon } from "../icons/region.js";
6
+ import { TextIcon } from "../icons/text.js";
7
+ import { createElement, forwardRef } from "react";
8
+ import { useMaple, useMapleClient } from "@maple-kit/react";
9
+ //#region src/composer/target.ts
10
+ const CLOSE_LABEL = "Close the composer";
11
+ const DETAIL_ATTRIBUTE = "data-mk-detail";
12
+ function iconFor(kind) {
13
+ if (kind === "text") return createElement(TextIcon, null);
14
+ return kind === "region" ? createElement(RegionIcon, null) : createElement(ElementIcon, null);
15
+ }
16
+ const MapleTarget = forwardRef(function MapleTarget(props, ref) {
17
+ const { composer } = useMaple();
18
+ const client = useMapleClient();
19
+ const Element = props.asChild ? Slot : "header";
20
+ return createElement(Element, {
21
+ ref,
22
+ className: props.className ? `mk-composer-head ${props.className}` : "mk-composer-head",
23
+ "data-mk-detail": "default"
24
+ }, composer.target ? createElement(TargetLine, { target: composer.target }) : null, createElement("span", { className: "mk-composer-fill" }), createElement("button", {
25
+ type: "button",
26
+ className: "mk-btn mk-btn-quiet mk-shut mk-hit mk-press",
27
+ "aria-label": CLOSE_LABEL,
28
+ onClick: () => client.closeComposer()
29
+ }, "✕"));
30
+ });
31
+ function TargetLine(props) {
32
+ const { kind } = props.target;
33
+ const quoted = quotedText(props.target);
34
+ const phrase = quoted ?? targetPhrase(kind, targetName(props.target));
35
+ return createElement("span", { className: "mk-target" }, createElement("span", {
36
+ className: "mk-target-kind",
37
+ role: "img",
38
+ "aria-label": KIND_WORDS[kind]
39
+ }, createElement(IconCrossfade, {
40
+ name: kind,
41
+ children: iconFor(kind)
42
+ })), createElement("span", { className: "mk-target-on" }, "on ", quoted === void 0 ? createElement("b", null, phrase) : createElement("q", { className: "mk-target-quote" }, quoted)));
43
+ }
44
+ //#endregion
45
+ export { CLOSE_LABEL, DETAIL_ATTRIBUTE, MapleTarget };
@@ -0,0 +1,21 @@
1
+ import { Scheme } from "@maple-kit/core/client";
2
+ //#region src/context.d.ts
3
+ /** The shadow root, and the two schemes, for the parts inside it. */
4
+ export interface MapleUiContextValue {
5
+ /** Where every part renders. One per `Maple.Root`, styled only by adoption. */
6
+ readonly root: ShadowRoot;
7
+ /** The element carrying the shadow root; `:host` in the stylesheet. */
8
+ readonly container: HTMLElement;
9
+ /** The overlay's scheme: the opposite of the host's. */
10
+ readonly scheme: Scheme;
11
+ /** The host page's scheme. This is the one a comment records. */
12
+ readonly hostScheme: Scheme;
13
+ }
14
+ /** Thrown by a part rendered outside `<Maple.Root>`. */
15
+ export declare class MapleUiContextError extends Error {
16
+ readonly name = "MapleUiContextError";
17
+ constructor(part: string);
18
+ }
19
+ /** The shadow root and the schemes, or a readable error rather than a null. */
20
+ export declare function useMapleUi(part: string): MapleUiContextValue;
21
+ //#endregion
@@ -0,0 +1,16 @@
1
+ import { createContext, useContext } from "react";
2
+ //#region src/context.ts
3
+ const MapleUiContext = createContext(null);
4
+ var MapleUiContextError = class extends Error {
5
+ name = "MapleUiContextError";
6
+ constructor(part) {
7
+ super(`${part} was rendered outside <Maple.Root>. Every part needs one above it.`);
8
+ }
9
+ };
10
+ function useMapleUi(part) {
11
+ const value = useContext(MapleUiContext);
12
+ if (value === null) throw new MapleUiContextError(part);
13
+ return value;
14
+ }
15
+ //#endregion
16
+ export { MapleUiContext, MapleUiContextError, useMapleUi };