@maple-kit/ui 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +202 -0
- package/NOTICE +9 -0
- package/README.md +39 -0
- package/dist/composer/actions.d.ts +15 -0
- package/dist/composer/actions.js +59 -0
- package/dist/composer/attachments.d.ts +33 -0
- package/dist/composer/attachments.js +166 -0
- package/dist/composer/badge.d.ts +13 -0
- package/dist/composer/badge.js +25 -0
- package/dist/composer/body.d.ts +13 -0
- package/dist/composer/body.js +119 -0
- package/dist/composer/composer.d.ts +23 -0
- package/dist/composer/composer.js +122 -0
- package/dist/composer/css.js +559 -0
- package/dist/composer/detail.d.ts +32 -0
- package/dist/composer/detail.js +102 -0
- package/dist/composer/emoji.d.ts +54 -0
- package/dist/composer/emoji.js +131 -0
- package/dist/composer/index.d.ts +15 -0
- package/dist/composer/index.js +15 -0
- package/dist/composer/inset.js +20 -0
- package/dist/composer/leave.d.ts +19 -0
- package/dist/composer/leave.js +34 -0
- package/dist/composer/peek.d.ts +21 -0
- package/dist/composer/peek.js +29 -0
- package/dist/composer/phrase.d.ts +27 -0
- package/dist/composer/phrase.js +30 -0
- package/dist/composer/picker-emoji.d.ts +34 -0
- package/dist/composer/picker-emoji.js +48 -0
- package/dist/composer/prompt.js +31 -0
- package/dist/composer/scope.d.ts +11 -0
- package/dist/composer/scope.js +16 -0
- package/dist/composer/target.d.ts +13 -0
- package/dist/composer/target.js +45 -0
- package/dist/context.d.ts +21 -0
- package/dist/context.js +16 -0
- package/dist/data.d.ts +52 -0
- package/dist/data.js +30 -0
- package/dist/icons/branch.d.ts +4 -0
- package/dist/icons/branch.js +8 -0
- package/dist/icons/cog.d.ts +4 -0
- package/dist/icons/cog.js +8 -0
- package/dist/icons/commit.d.ts +4 -0
- package/dist/icons/commit.js +5 -0
- package/dist/icons/crossfade.d.ts +15 -0
- package/dist/icons/crossfade.js +27 -0
- package/dist/icons/element.d.ts +4 -0
- package/dist/icons/element.js +5 -0
- package/dist/icons/icon.d.ts +24 -0
- package/dist/icons/icon.js +27 -0
- package/dist/icons/index.d.ts +13 -0
- package/dist/icons/index.js +13 -0
- package/dist/icons/noplace.d.ts +4 -0
- package/dist/icons/noplace.js +5 -0
- package/dist/icons/region.d.ts +4 -0
- package/dist/icons/region.js +8 -0
- package/dist/icons/smiley.d.ts +4 -0
- package/dist/icons/smiley.js +8 -0
- package/dist/icons/sparkle.d.ts +4 -0
- package/dist/icons/sparkle.js +8 -0
- package/dist/icons/target.d.ts +4 -0
- package/dist/icons/target.js +5 -0
- package/dist/icons/text.d.ts +4 -0
- package/dist/icons/text.js +5 -0
- package/dist/index.d.ts +11 -0
- package/dist/index.js +11 -0
- package/dist/island/account.d.ts +10 -0
- package/dist/island/account.js +52 -0
- package/dist/island/comments.d.ts +24 -0
- package/dist/island/comments.js +40 -0
- package/dist/island/content.d.ts +10 -0
- package/dist/island/content.js +26 -0
- package/dist/island/context.d.ts +58 -0
- package/dist/island/context.js +23 -0
- package/dist/island/css.d.ts +13 -0
- package/dist/island/css.js +1020 -0
- package/dist/island/drag.d.ts +10 -0
- package/dist/island/drag.js +77 -0
- package/dist/island/filters.d.ts +10 -0
- package/dist/island/filters.js +63 -0
- package/dist/island/header.d.ts +32 -0
- package/dist/island/header.js +47 -0
- package/dist/island/index.d.ts +18 -0
- package/dist/island/index.js +18 -0
- package/dist/island/island.d.ts +12 -0
- package/dist/island/island.js +111 -0
- package/dist/island/item.d.ts +12 -0
- package/dist/island/item.js +158 -0
- package/dist/island/language.d.ts +55 -0
- package/dist/island/language.js +125 -0
- package/dist/island/leaf.js +17 -0
- package/dist/island/list.d.ts +12 -0
- package/dist/island/list.js +31 -0
- package/dist/island/new-comment.d.ts +18 -0
- package/dist/island/new-comment.js +56 -0
- package/dist/island/settings.d.ts +10 -0
- package/dist/island/settings.js +134 -0
- package/dist/island/stagger.d.ts +12 -0
- package/dist/island/stagger.js +4 -0
- package/dist/island/time.d.ts +17 -0
- package/dist/island/time.js +29 -0
- package/dist/island/trigger.d.ts +10 -0
- package/dist/island/trigger.js +39 -0
- package/dist/language.d.ts +45 -0
- package/dist/language.js +66 -0
- package/dist/maple.d.ts +25 -0
- package/dist/maple.js +65 -0
- package/dist/marks/avatar.d.ts +16 -0
- package/dist/marks/avatar.js +36 -0
- package/dist/marks/css.d.ts +12 -0
- package/dist/marks/css.js +371 -0
- package/dist/marks/frame.d.ts +25 -0
- package/dist/marks/frame.js +53 -0
- package/dist/marks/geometry.d.ts +55 -0
- package/dist/marks/geometry.js +61 -0
- package/dist/marks/index.d.ts +14 -0
- package/dist/marks/index.js +14 -0
- package/dist/marks/label.d.ts +23 -0
- package/dist/marks/label.js +31 -0
- package/dist/marks/layer.d.ts +16 -0
- package/dist/marks/layer.js +194 -0
- package/dist/marks/leaf.d.ts +18 -0
- package/dist/marks/leaf.js +7 -0
- package/dist/marks/mark.d.ts +28 -0
- package/dist/marks/mark.js +43 -0
- package/dist/marks/nudge.d.ts +50 -0
- package/dist/marks/nudge.js +76 -0
- package/dist/marks/paint.d.ts +11 -0
- package/dist/marks/paint.js +19 -0
- package/dist/marks/placement.d.ts +24 -0
- package/dist/marks/placement.js +28 -0
- package/dist/marks/ring.d.ts +28 -0
- package/dist/marks/ring.js +85 -0
- package/dist/marks/shape.d.ts +21 -0
- package/dist/marks/shape.js +51 -0
- package/dist/notice/css.d.ts +11 -0
- package/dist/notice/css.js +74 -0
- package/dist/notice/index.d.ts +4 -0
- package/dist/notice/index.js +4 -0
- package/dist/notice/language.d.ts +15 -0
- package/dist/notice/language.js +14 -0
- package/dist/notice/notice.d.ts +14 -0
- package/dist/notice/notice.js +49 -0
- package/dist/part.d.ts +9 -0
- package/dist/part.js +11 -0
- package/dist/picker/css.d.ts +13 -0
- package/dist/picker/css.js +141 -0
- package/dist/picker/index.d.ts +6 -0
- package/dist/picker/index.js +6 -0
- package/dist/picker/language.d.ts +14 -0
- package/dist/picker/language.js +17 -0
- package/dist/picker/picker.d.ts +18 -0
- package/dist/picker/picker.js +164 -0
- package/dist/picker/session.d.ts +16 -0
- package/dist/picker/session.js +35 -0
- package/dist/picker/target.d.ts +23 -0
- package/dist/picker/target.js +37 -0
- package/dist/root.d.ts +42 -0
- package/dist/root.js +75 -0
- package/dist/shots.js +32 -0
- package/dist/slot.d.ts +28 -0
- package/dist/slot.js +52 -0
- package/dist/slots.d.ts +24 -0
- package/dist/slots.js +75 -0
- package/dist/stylesheet.d.ts +15 -0
- package/dist/stylesheet.js +188 -0
- package/dist/theme.d.ts +20 -0
- package/dist/theme.js +13 -0
- package/dist/tip.d.ts +40 -0
- package/dist/tip.js +82 -0
- package/dist/tokens.d.ts +28 -0
- package/dist/tokens.js +180 -0
- package/package.json +56 -0
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { DragHandlers } from "./context.js";
|
|
2
|
+
import { Corner } from "@maple-kit/core/client";
|
|
3
|
+
//#region src/island/drag.d.ts
|
|
4
|
+
/** How far a pointer travels before it is a drag and not a slow click. */
|
|
5
|
+
export declare const DRAG_THRESHOLD_PX = 4;
|
|
6
|
+
/** Set on the island while it is under the pointer. */
|
|
7
|
+
export declare const DRAGGING_ATTRIBUTE = "data-mk-dragging";
|
|
8
|
+
/** Moves the island with the pointer, and hands the corner to the controller. */
|
|
9
|
+
export declare function useDrag(container: HTMLElement, onSnap: (corner: Corner) => void): DragHandlers;
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,77 @@
|
|
|
1
|
+
import { useMemo, useRef } from "react";
|
|
2
|
+
import { nearestCorner } from "@maple-kit/core/client";
|
|
3
|
+
//#region src/island/drag.ts
|
|
4
|
+
const DRAG_THRESHOLD_PX = 4;
|
|
5
|
+
const DRAGGING_ATTRIBUTE = "data-mk-dragging";
|
|
6
|
+
function useDrag(container, onSnap) {
|
|
7
|
+
const gesture = useRef({
|
|
8
|
+
node: null,
|
|
9
|
+
x: 0,
|
|
10
|
+
y: 0,
|
|
11
|
+
pressed: false,
|
|
12
|
+
dragging: false,
|
|
13
|
+
moved: false
|
|
14
|
+
});
|
|
15
|
+
return useMemo(() => ({
|
|
16
|
+
attach: (node) => {
|
|
17
|
+
gesture.current.node = node;
|
|
18
|
+
},
|
|
19
|
+
moved: () => gesture.current.moved,
|
|
20
|
+
onPointerDown: (event) => start(gesture.current, event),
|
|
21
|
+
onPointerMove: (event) => move(gesture.current, event),
|
|
22
|
+
onPointerUp: (event) => end(gesture.current, event, container, onSnap)
|
|
23
|
+
}), [container, onSnap]);
|
|
24
|
+
}
|
|
25
|
+
function start(gesture, event) {
|
|
26
|
+
gesture.x = event.clientX;
|
|
27
|
+
gesture.y = event.clientY;
|
|
28
|
+
gesture.pressed = true;
|
|
29
|
+
gesture.dragging = false;
|
|
30
|
+
gesture.moved = false;
|
|
31
|
+
capture(event, true);
|
|
32
|
+
}
|
|
33
|
+
function capture(event, take) {
|
|
34
|
+
const element = handle(event);
|
|
35
|
+
try {
|
|
36
|
+
if (take) element?.setPointerCapture(event.pointerId);
|
|
37
|
+
else element?.releasePointerCapture(event.pointerId);
|
|
38
|
+
} catch {
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
function move(gesture, event) {
|
|
43
|
+
const { node } = gesture;
|
|
44
|
+
if (!node || !gesture.pressed) return;
|
|
45
|
+
const dx = event.clientX - gesture.x;
|
|
46
|
+
const dy = event.clientY - gesture.y;
|
|
47
|
+
if (!gesture.dragging && Math.hypot(dx, dy) < 4) return;
|
|
48
|
+
gesture.dragging = true;
|
|
49
|
+
gesture.moved = true;
|
|
50
|
+
node.style.setProperty("--mk-x", `${String(Math.round(dx))}px`);
|
|
51
|
+
node.style.setProperty("--mk-y", `${String(Math.round(dy))}px`);
|
|
52
|
+
node.setAttribute(DRAGGING_ATTRIBUTE, "true");
|
|
53
|
+
}
|
|
54
|
+
function end(gesture, event, container, onSnap) {
|
|
55
|
+
const { node } = gesture;
|
|
56
|
+
capture(event, false);
|
|
57
|
+
gesture.pressed = false;
|
|
58
|
+
if (!node || !gesture.dragging) return;
|
|
59
|
+
gesture.dragging = false;
|
|
60
|
+
const box = node.getBoundingClientRect();
|
|
61
|
+
node.removeAttribute(DRAGGING_ATTRIBUTE);
|
|
62
|
+
node.style.removeProperty("--mk-x");
|
|
63
|
+
node.style.removeProperty("--mk-y");
|
|
64
|
+
onSnap(nearestCorner(middleOf(box), container.getBoundingClientRect()));
|
|
65
|
+
}
|
|
66
|
+
function middleOf(box) {
|
|
67
|
+
return {
|
|
68
|
+
x: box.x + box.width / 2,
|
|
69
|
+
y: box.y + box.height / 2
|
|
70
|
+
};
|
|
71
|
+
}
|
|
72
|
+
function handle(event) {
|
|
73
|
+
const target = event.currentTarget;
|
|
74
|
+
return target instanceof Element ? target : null;
|
|
75
|
+
}
|
|
76
|
+
//#endregion
|
|
77
|
+
export { DRAGGING_ATTRIBUTE, DRAG_THRESHOLD_PX, useDrag };
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { PartProps } from "../part.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
//#region src/island/filters.d.ts
|
|
4
|
+
/** The filter row. Its own children replace the select and the tally. */
|
|
5
|
+
export interface FiltersProps extends PartProps {
|
|
6
|
+
readonly children?: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
/** Every filter in one select, with the per-status tally beside it. */
|
|
9
|
+
export declare const Filters: import("react").ForwardRefExoticComponent<FiltersProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
10
|
+
//#endregion
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { cx, renderPart } from "../part.js";
|
|
2
|
+
import { Tip } from "../tip.js";
|
|
3
|
+
import { FILTERS_LABEL, FILTER_LABELS, FILTER_ORDER, TALLY_ORDER, tallyTitle } from "./language.js";
|
|
4
|
+
import { countsFor } from "./comments.js";
|
|
5
|
+
import { listId, useIsland } from "./context.js";
|
|
6
|
+
import { createElement, forwardRef } from "react";
|
|
7
|
+
import { useMaple, useMapleClient } from "@maple-kit/react";
|
|
8
|
+
//#region src/island/filters.ts
|
|
9
|
+
const PART = "<Maple.Filters>";
|
|
10
|
+
const Filters = forwardRef(function Filters(props, ref) {
|
|
11
|
+
const { asChild, children, className, ...rest } = props;
|
|
12
|
+
const island = useIsland(PART);
|
|
13
|
+
const { comments, filter, showResolved } = useMaple();
|
|
14
|
+
const client = useMapleClient();
|
|
15
|
+
const counts = countsFor(comments, showResolved);
|
|
16
|
+
const onChange = (event) => {
|
|
17
|
+
client.setFilter(event.currentTarget.value);
|
|
18
|
+
};
|
|
19
|
+
const select = renderPart("span", false, {
|
|
20
|
+
key: "chip",
|
|
21
|
+
className: "mk-filter-chip"
|
|
22
|
+
}, renderPart("select", false, {
|
|
23
|
+
className: "mk-filter-pick",
|
|
24
|
+
"aria-controls": listId(island.contentId),
|
|
25
|
+
"aria-label": FILTERS_LABEL,
|
|
26
|
+
value: filter,
|
|
27
|
+
onChange
|
|
28
|
+
}, ALL_FILTERS.map((name) => renderPart("option", false, {
|
|
29
|
+
key: name,
|
|
30
|
+
value: name
|
|
31
|
+
}, `${FILTER_LABELS[name]} ${String(counts[name])}`))));
|
|
32
|
+
const tally = renderPart("div", false, {
|
|
33
|
+
key: "tally",
|
|
34
|
+
className: "mk-tally"
|
|
35
|
+
}, TALLY_ORDER.map((name) => createElement(Tip, {
|
|
36
|
+
key: name,
|
|
37
|
+
as: "button",
|
|
38
|
+
className: "mk-tally-one",
|
|
39
|
+
sentence: tallyTitle(name, counts[name]),
|
|
40
|
+
triggerProps: {
|
|
41
|
+
type: "button",
|
|
42
|
+
"data-tally": name,
|
|
43
|
+
"data-mk-zero": String(counts[name] === 0),
|
|
44
|
+
"aria-pressed": filter === name,
|
|
45
|
+
"aria-label": tallyTitle(name, counts[name]),
|
|
46
|
+
onClick: () => client.setFilter(filter === name ? "all" : name)
|
|
47
|
+
}
|
|
48
|
+
}, renderPart("span", false, {
|
|
49
|
+
key: "dot",
|
|
50
|
+
className: "mk-dot"
|
|
51
|
+
}), renderPart("span", false, {
|
|
52
|
+
key: "n",
|
|
53
|
+
className: "mk-num"
|
|
54
|
+
}, String(counts[name])))));
|
|
55
|
+
return renderPart("div", asChild, {
|
|
56
|
+
...rest,
|
|
57
|
+
className: cx("mk-filters", className),
|
|
58
|
+
ref
|
|
59
|
+
}, children ?? [select, tally]);
|
|
60
|
+
});
|
|
61
|
+
const ALL_FILTERS = [...FILTER_ORDER, "unpinned"];
|
|
62
|
+
//#endregion
|
|
63
|
+
export { Filters };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { PartProps } from "../part.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
//#region src/island/header.d.ts
|
|
4
|
+
/** The header row. Its children sit before the close control. */
|
|
5
|
+
export interface HeaderProps extends PartProps {
|
|
6
|
+
readonly children?: ReactNode;
|
|
7
|
+
}
|
|
8
|
+
/** The wordmark, and the word. */
|
|
9
|
+
export interface LogoProps extends PartProps {
|
|
10
|
+
/** Replaces the title beside the leaf. */
|
|
11
|
+
readonly children?: ReactNode;
|
|
12
|
+
}
|
|
13
|
+
/** Which branch these comments belong to. */
|
|
14
|
+
export interface BranchProps extends PartProps {
|
|
15
|
+
/** The branch the island is pointed at, as the chip should read it. */
|
|
16
|
+
readonly branch?: string;
|
|
17
|
+
/** Shown instead of the branch, which stays on the chip as its title. */
|
|
18
|
+
readonly label?: string;
|
|
19
|
+
readonly children?: ReactNode;
|
|
20
|
+
}
|
|
21
|
+
/** The header, with the close control after whatever it was given. */
|
|
22
|
+
export declare const Header: import("react").ForwardRefExoticComponent<HeaderProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
23
|
+
/** The leaf and the word "Comments", which is what the island is. */
|
|
24
|
+
export declare const Logo: import("react").ForwardRefExoticComponent<LogoProps & import("react").RefAttributes<HTMLHeadingElement>>;
|
|
25
|
+
/**
|
|
26
|
+
* The branch chip. It takes the branch rather than reading it, because the
|
|
27
|
+
* controller keeps what it was pointed at and does not publish it back. A
|
|
28
|
+
* label reads in its place when there is one; the branch stays as the title,
|
|
29
|
+
* because a label is lossy and the exact name is what a person copies.
|
|
30
|
+
*/
|
|
31
|
+
export declare const Branch: import("react").ForwardRefExoticComponent<BranchProps & import("react").RefAttributes<HTMLSpanElement>>;
|
|
32
|
+
//#endregion
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
import { cx, renderPart } from "../part.js";
|
|
2
|
+
import { ISLAND_COPY } from "./language.js";
|
|
3
|
+
import { useIsland } from "./context.js";
|
|
4
|
+
import { Leaf } from "./leaf.js";
|
|
5
|
+
import { forwardRef } from "react";
|
|
6
|
+
//#region src/island/header.ts
|
|
7
|
+
const HEADER_PART = "<Maple.Header>";
|
|
8
|
+
const Header = forwardRef(function Header(props, ref) {
|
|
9
|
+
const { asChild, children, className, ...rest } = props;
|
|
10
|
+
const island = useIsland(HEADER_PART);
|
|
11
|
+
return renderPart("div", asChild, {
|
|
12
|
+
...rest,
|
|
13
|
+
className: cx("mk-head", className),
|
|
14
|
+
ref
|
|
15
|
+
}, [children, renderPart("button", false, {
|
|
16
|
+
key: "close",
|
|
17
|
+
type: "button",
|
|
18
|
+
"aria-label": ISLAND_COPY.close,
|
|
19
|
+
className: "mk-iconbtn mk-hit",
|
|
20
|
+
onClick: () => island.setOpen(false)
|
|
21
|
+
}, ISLAND_COPY.closeGlyph)]);
|
|
22
|
+
});
|
|
23
|
+
const Logo = forwardRef(function Logo(props, ref) {
|
|
24
|
+
const { asChild, children, className, ...rest } = props;
|
|
25
|
+
return renderPart("h2", asChild, {
|
|
26
|
+
...rest,
|
|
27
|
+
className: cx("mk-head-title", className),
|
|
28
|
+
ref
|
|
29
|
+
}, [renderPart(Leaf, false, {
|
|
30
|
+
key: "leaf",
|
|
31
|
+
size: 17,
|
|
32
|
+
className: "mk-logo-leaf"
|
|
33
|
+
}), children ?? ISLAND_COPY.title]);
|
|
34
|
+
});
|
|
35
|
+
const Branch = forwardRef(function Branch(props, ref) {
|
|
36
|
+
const { asChild, branch, children, className, label, ...rest } = props;
|
|
37
|
+
const shown = children ?? label ?? branch;
|
|
38
|
+
if (shown === void 0) return null;
|
|
39
|
+
return renderPart("span", asChild, {
|
|
40
|
+
...rest,
|
|
41
|
+
className: cx("mk-branch", className),
|
|
42
|
+
title: branch,
|
|
43
|
+
ref
|
|
44
|
+
}, shown);
|
|
45
|
+
});
|
|
46
|
+
//#endregion
|
|
47
|
+
export { Branch, Header, Logo };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { Account, AccountProps } from "./account.js";
|
|
2
|
+
import { FilterCounts, countsFor, numbersFor, orphanReason, resolutionsFor } from "./comments.js";
|
|
3
|
+
import { IslandContent, IslandContentProps } from "./content.js";
|
|
4
|
+
import { DragHandlers, IslandContextError, IslandContextValue, IslandPhase, PointerEventLike, reasonOf, useIsland } from "./context.js";
|
|
5
|
+
import { islandCss } from "./css.js";
|
|
6
|
+
import { DRAGGING_ATTRIBUTE, DRAG_THRESHOLD_PX, useDrag } from "./drag.js";
|
|
7
|
+
import { Filters, FiltersProps } from "./filters.js";
|
|
8
|
+
import { Branch, BranchProps, Header, HeaderProps, Logo, LogoProps } from "./header.js";
|
|
9
|
+
import { Island, IslandProps } from "./island.js";
|
|
10
|
+
import { Item, ItemProps, TIME_DELAY_MS } from "./item.js";
|
|
11
|
+
import { FILTER_LABELS, ISLAND_COPY, SETTINGS_COPY, kindPhrase, openLabel } from "./language.js";
|
|
12
|
+
import { List, ListProps } from "./list.js";
|
|
13
|
+
import { NewComment, NewCommentProps, PickButton, PickButtonProps } from "./new-comment.js";
|
|
14
|
+
import { Settings, SettingsProps } from "./settings.js";
|
|
15
|
+
import { STAGGER_ROWS } from "./stagger.js";
|
|
16
|
+
import { absoluteTime, relativeTime } from "./time.js";
|
|
17
|
+
import { IslandTrigger, IslandTriggerProps } from "./trigger.js";
|
|
18
|
+
export { Account, type AccountProps, Branch, type BranchProps, DRAGGING_ATTRIBUTE, DRAG_THRESHOLD_PX, type DragHandlers, FILTER_LABELS, type FilterCounts, Filters, type FiltersProps, Header, type HeaderProps, ISLAND_COPY, Island, IslandContent, type IslandContentProps, IslandContextError, type IslandContextValue, type IslandPhase, type IslandProps, IslandTrigger, type IslandTriggerProps, Item, type ItemProps, List, type ListProps, Logo, type LogoProps, NewComment, type NewCommentProps, PickButton, type PickButtonProps, type PointerEventLike, SETTINGS_COPY, STAGGER_ROWS, Settings, type SettingsProps, TIME_DELAY_MS, absoluteTime, countsFor, islandCss, kindPhrase, numbersFor, openLabel, orphanReason, reasonOf, relativeTime, resolutionsFor, useDrag, useIsland };
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
import { STAGGER_ROWS } from "./stagger.js";
|
|
2
|
+
import { islandCss } from "./css.js";
|
|
3
|
+
import { FILTER_LABELS, ISLAND_COPY, SETTINGS_COPY, kindPhrase, openLabel } from "./language.js";
|
|
4
|
+
import { Account } from "./account.js";
|
|
5
|
+
import { countsFor, numbersFor, orphanReason, resolutionsFor } from "./comments.js";
|
|
6
|
+
import { IslandContextError, reasonOf, useIsland } from "./context.js";
|
|
7
|
+
import { IslandContent } from "./content.js";
|
|
8
|
+
import { DRAGGING_ATTRIBUTE, DRAG_THRESHOLD_PX, useDrag } from "./drag.js";
|
|
9
|
+
import { Filters } from "./filters.js";
|
|
10
|
+
import { Branch, Header, Logo } from "./header.js";
|
|
11
|
+
import { Island } from "./island.js";
|
|
12
|
+
import { absoluteTime, relativeTime } from "./time.js";
|
|
13
|
+
import { Item, TIME_DELAY_MS } from "./item.js";
|
|
14
|
+
import { List } from "./list.js";
|
|
15
|
+
import { NewComment, PickButton } from "./new-comment.js";
|
|
16
|
+
import { Settings } from "./settings.js";
|
|
17
|
+
import { IslandTrigger } from "./trigger.js";
|
|
18
|
+
export { Account, Branch, DRAGGING_ATTRIBUTE, DRAG_THRESHOLD_PX, FILTER_LABELS, Filters, Header, ISLAND_COPY, Island, IslandContent, IslandContextError, IslandTrigger, Item, List, Logo, NewComment, PickButton, SETTINGS_COPY, STAGGER_ROWS, Settings, TIME_DELAY_MS, absoluteTime, countsFor, islandCss, kindPhrase, numbersFor, openLabel, orphanReason, reasonOf, relativeTime, resolutionsFor, useDrag, useIsland };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PartProps } from "../part.js";
|
|
2
|
+
import { ReactNode } from "react";
|
|
3
|
+
//#region src/island/island.d.ts
|
|
4
|
+
/** How the island starts, and what goes inside it. */
|
|
5
|
+
export interface IslandProps extends PartProps {
|
|
6
|
+
readonly children?: ReactNode;
|
|
7
|
+
/** Collapsed unless something already asked for it open. */
|
|
8
|
+
readonly defaultOpen?: boolean;
|
|
9
|
+
}
|
|
10
|
+
/** The island's own corner, and the state its parts share. */
|
|
11
|
+
export declare const Island: import("react").ForwardRefExoticComponent<IslandProps & import("react").RefAttributes<HTMLDivElement>>;
|
|
12
|
+
//#endregion
|
|
@@ -0,0 +1,111 @@
|
|
|
1
|
+
import { useMapleUi } from "../context.js";
|
|
2
|
+
import { composeRefs } from "../slot.js";
|
|
3
|
+
import { cx, renderPart } from "../part.js";
|
|
4
|
+
import { numbersFor, resolutionsFor } from "./comments.js";
|
|
5
|
+
import { IslandContext } from "./context.js";
|
|
6
|
+
import { useDrag } from "./drag.js";
|
|
7
|
+
import { createElement, forwardRef, useCallback, useEffect, useId, useMemo, useRef, useState } from "react";
|
|
8
|
+
import { watchEscape } from "@maple-kit/core/client";
|
|
9
|
+
import { useMaple, useMapleClient } from "@maple-kit/react";
|
|
10
|
+
//#region src/island/island.ts
|
|
11
|
+
const PART = "<Maple.Island>";
|
|
12
|
+
const Island = forwardRef(function Island(props, ref) {
|
|
13
|
+
const { asChild, children, className, defaultOpen, ...rest } = props;
|
|
14
|
+
const { composer, hidden, position } = useMaple();
|
|
15
|
+
const value = useIslandState(defaultOpen === true);
|
|
16
|
+
if (hidden) return null;
|
|
17
|
+
const element = renderPart("div", asChild, {
|
|
18
|
+
...rest,
|
|
19
|
+
className: cx("mk-island", className),
|
|
20
|
+
"data-mk-open": String(value.phase !== "closed"),
|
|
21
|
+
"data-mk-corner": position,
|
|
22
|
+
"data-mk-inset": String(composer.open),
|
|
23
|
+
ref: composeRefs(ref, value.drag.attach)
|
|
24
|
+
}, children);
|
|
25
|
+
return createElement(IslandContext.Provider, { value }, element);
|
|
26
|
+
});
|
|
27
|
+
function useIslandState(defaultOpen) {
|
|
28
|
+
const { comments, composer, detail, selected } = useMaple();
|
|
29
|
+
const client = useMapleClient();
|
|
30
|
+
const { container } = useMapleUi(PART);
|
|
31
|
+
const [phase, setPhase] = useState(defaultOpen ? "open" : "closed");
|
|
32
|
+
const [settingsOpen, setSettingsOpen] = useState(false);
|
|
33
|
+
const contentId = useId();
|
|
34
|
+
const setOpen = useCallback((open) => {
|
|
35
|
+
if (!open) client.select(null);
|
|
36
|
+
setPhase((current) => open ? "open" : leaving(current));
|
|
37
|
+
}, [client]);
|
|
38
|
+
const settled = useCallback(() => {
|
|
39
|
+
setPhase((current) => current === "closing" ? "closed" : current);
|
|
40
|
+
}, []);
|
|
41
|
+
const setDeveloper = useCallback((on) => {
|
|
42
|
+
client.setDetail(on ? "developer" : "default");
|
|
43
|
+
}, [client]);
|
|
44
|
+
const developer = detail === "developer";
|
|
45
|
+
const numbers = useMemo(() => numbersFor(comments), [comments]);
|
|
46
|
+
const page = container.ownerDocument;
|
|
47
|
+
const resolutions = useMemo(() => resolutionsFor(comments, page, developer), [
|
|
48
|
+
comments,
|
|
49
|
+
page,
|
|
50
|
+
developer
|
|
51
|
+
]);
|
|
52
|
+
const snap = useCallback((corner) => client.setPosition(corner), [client]);
|
|
53
|
+
const drag = useDrag(container, snap);
|
|
54
|
+
useEscape({
|
|
55
|
+
composerOpen: composer.open,
|
|
56
|
+
settingsOpen,
|
|
57
|
+
setSettingsOpen,
|
|
58
|
+
setOpen,
|
|
59
|
+
client
|
|
60
|
+
});
|
|
61
|
+
return useMemo(() => ({
|
|
62
|
+
phase: selected === null ? phase : "open",
|
|
63
|
+
setOpen,
|
|
64
|
+
settled,
|
|
65
|
+
developer,
|
|
66
|
+
setDeveloper,
|
|
67
|
+
settingsOpen,
|
|
68
|
+
setSettingsOpen,
|
|
69
|
+
contentId,
|
|
70
|
+
numbers,
|
|
71
|
+
comments,
|
|
72
|
+
resolutions,
|
|
73
|
+
selected,
|
|
74
|
+
drag
|
|
75
|
+
}), [
|
|
76
|
+
comments,
|
|
77
|
+
contentId,
|
|
78
|
+
developer,
|
|
79
|
+
drag,
|
|
80
|
+
numbers,
|
|
81
|
+
phase,
|
|
82
|
+
resolutions,
|
|
83
|
+
selected,
|
|
84
|
+
setDeveloper,
|
|
85
|
+
setOpen,
|
|
86
|
+
settingsOpen,
|
|
87
|
+
settled
|
|
88
|
+
]);
|
|
89
|
+
}
|
|
90
|
+
function useEscape(order) {
|
|
91
|
+
const latest = useRef(order);
|
|
92
|
+
latest.current = order;
|
|
93
|
+
useEffect(() => {
|
|
94
|
+
const stop = new AbortController();
|
|
95
|
+
watchEscape({
|
|
96
|
+
signal: stop.signal,
|
|
97
|
+
onEscape: () => {
|
|
98
|
+
const now = latest.current;
|
|
99
|
+
if (now.composerOpen) return now.client.closeComposer();
|
|
100
|
+
if (now.settingsOpen) return now.setSettingsOpen(false);
|
|
101
|
+
now.setOpen(false);
|
|
102
|
+
}
|
|
103
|
+
});
|
|
104
|
+
return () => stop.abort();
|
|
105
|
+
}, []);
|
|
106
|
+
}
|
|
107
|
+
function leaving(phase) {
|
|
108
|
+
return phase === "open" ? "closing" : phase;
|
|
109
|
+
}
|
|
110
|
+
//#endregion
|
|
111
|
+
export { Island };
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { PartProps } from "../part.js";
|
|
2
|
+
import { Comment } from "@maple-kit/core";
|
|
3
|
+
//#region src/island/item.d.ts
|
|
4
|
+
/** One comment. The row renders it; it takes no view of its own. */
|
|
5
|
+
export interface ItemProps extends PartProps {
|
|
6
|
+
readonly comment: Comment;
|
|
7
|
+
}
|
|
8
|
+
/** A second of rest: nobody hovers a timestamp meaning to ask what it was. */
|
|
9
|
+
export declare const TIME_DELAY_MS = 1000;
|
|
10
|
+
/** A row: which comment, who and when, what they said, and what it is on. */
|
|
11
|
+
export declare const Item: import("react").ForwardRefExoticComponent<ItemProps & import("react").RefAttributes<HTMLElement>>;
|
|
12
|
+
//#endregion
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { dataAttributes, formFor } from "../data.js";
|
|
2
|
+
import { PROVENANCE_SENTENCES, STATUS_LABELS, STATUS_SENTENCES } from "../language.js";
|
|
3
|
+
import { composeRefs } from "../slot.js";
|
|
4
|
+
import { applyReviewerSlot } from "../slots.js";
|
|
5
|
+
import { cx, renderPart } from "../part.js";
|
|
6
|
+
import { Tip } from "../tip.js";
|
|
7
|
+
import { ISLAND_COPY, kindPhrase } from "./language.js";
|
|
8
|
+
import { useIsland } from "./context.js";
|
|
9
|
+
import { MapleLeaf } from "../marks/shape.js";
|
|
10
|
+
import { absoluteTime, relativeTime } from "./time.js";
|
|
11
|
+
import { createElement, forwardRef, useCallback, useRef, useState } from "react";
|
|
12
|
+
import { useMapleClient } from "@maple-kit/react";
|
|
13
|
+
import { kindOf, labelFor } from "@maple-kit/core/anchor";
|
|
14
|
+
//#region src/island/item.ts
|
|
15
|
+
const LONG_BODY = 150;
|
|
16
|
+
const PART = "<Maple.Item>";
|
|
17
|
+
const TIME_DELAY_MS = 1e3;
|
|
18
|
+
const Item = forwardRef(function Item(props, ref) {
|
|
19
|
+
const { asChild, className, comment, ...rest } = props;
|
|
20
|
+
const island = useIsland(PART);
|
|
21
|
+
const client = useMapleClient();
|
|
22
|
+
const [expanded, setExpanded] = useState(false);
|
|
23
|
+
const long = comment.body.length > LONG_BODY;
|
|
24
|
+
const number = island.numbers.get(comment.id) ?? 0;
|
|
25
|
+
const selected = island.selected === comment.id;
|
|
26
|
+
return renderPart("article", asChild, {
|
|
27
|
+
...dataAttributes({ status: comment.status }),
|
|
28
|
+
...rest,
|
|
29
|
+
"data-mk-expanded": String(expanded),
|
|
30
|
+
"data-mk-selected": String(selected),
|
|
31
|
+
className: cx("mk-row", className),
|
|
32
|
+
onClick: () => client.viewComment(comment.id),
|
|
33
|
+
onPointerEnter: () => client.peek(comment.id),
|
|
34
|
+
onPointerLeave: () => client.peek(null),
|
|
35
|
+
onFocus: () => client.peek(comment.id),
|
|
36
|
+
onBlur: () => client.peek(null),
|
|
37
|
+
ref: composeRefs(ref, useReveal(selected))
|
|
38
|
+
}, [
|
|
39
|
+
top(comment, number),
|
|
40
|
+
renderPart("p", false, {
|
|
41
|
+
key: "body",
|
|
42
|
+
className: "mk-text mk-body"
|
|
43
|
+
}, comment.body),
|
|
44
|
+
long ? more(expanded, () => setExpanded(!expanded)) : null,
|
|
45
|
+
meta(comment)
|
|
46
|
+
]);
|
|
47
|
+
});
|
|
48
|
+
function useReveal(selected) {
|
|
49
|
+
const shown = useRef(false);
|
|
50
|
+
return useCallback((node) => {
|
|
51
|
+
if (!node || !selected) {
|
|
52
|
+
shown.current = shown.current && selected;
|
|
53
|
+
return;
|
|
54
|
+
}
|
|
55
|
+
if (shown.current) return;
|
|
56
|
+
shown.current = true;
|
|
57
|
+
node.scrollIntoView({
|
|
58
|
+
block: "nearest",
|
|
59
|
+
behavior: "smooth"
|
|
60
|
+
});
|
|
61
|
+
}, [selected]);
|
|
62
|
+
}
|
|
63
|
+
function more(expanded, onClick) {
|
|
64
|
+
const label = expanded ? ISLAND_COPY.showLess : ISLAND_COPY.showAll;
|
|
65
|
+
return renderPart("button", false, {
|
|
66
|
+
key: "more",
|
|
67
|
+
type: "button",
|
|
68
|
+
className: "mk-more",
|
|
69
|
+
onClick
|
|
70
|
+
}, label);
|
|
71
|
+
}
|
|
72
|
+
function top(comment, number) {
|
|
73
|
+
const chip = comment.status === "open" ? null : createElement(Tip, {
|
|
74
|
+
key: "status",
|
|
75
|
+
className: cx("mk-chip", chipTone(comment)),
|
|
76
|
+
sentence: STATUS_SENTENCES[comment.status]
|
|
77
|
+
}, STATUS_LABELS[comment.status]);
|
|
78
|
+
return renderPart("div", false, {
|
|
79
|
+
key: "top",
|
|
80
|
+
className: "mk-row-top"
|
|
81
|
+
}, [
|
|
82
|
+
pin(comment, number),
|
|
83
|
+
who(comment),
|
|
84
|
+
chip
|
|
85
|
+
]);
|
|
86
|
+
}
|
|
87
|
+
function chipTone(comment) {
|
|
88
|
+
if (comment.status === "resolved") return "mk-chip-ok";
|
|
89
|
+
return comment.status === "orphaned" ? "mk-chip-lost" : "mk-chip-warn";
|
|
90
|
+
}
|
|
91
|
+
function pin(comment, number) {
|
|
92
|
+
const form = formFor(comment.status);
|
|
93
|
+
const sentence = `${STATUS_LABELS[comment.status]} — ${STATUS_SENTENCES[comment.status]}`;
|
|
94
|
+
return createElement(Tip, {
|
|
95
|
+
key: "pin",
|
|
96
|
+
className: "mk-rowleaf",
|
|
97
|
+
sentence,
|
|
98
|
+
triggerProps: { ...dataAttributes({
|
|
99
|
+
status: comment.status,
|
|
100
|
+
form
|
|
101
|
+
}) }
|
|
102
|
+
}, createElement(MapleLeaf, {
|
|
103
|
+
key: "leaf",
|
|
104
|
+
form,
|
|
105
|
+
halo: false
|
|
106
|
+
}), createElement("span", {
|
|
107
|
+
key: "n",
|
|
108
|
+
className: "mk-mark-n mk-num",
|
|
109
|
+
"data-mk-digits": String(String(number).length)
|
|
110
|
+
}, number));
|
|
111
|
+
}
|
|
112
|
+
function who(comment) {
|
|
113
|
+
const { author } = comment;
|
|
114
|
+
return renderPart("span", false, {
|
|
115
|
+
key: "who",
|
|
116
|
+
className: "mk-who",
|
|
117
|
+
...dataAttributes({ provenance: author.provenance })
|
|
118
|
+
}, [name(author), createElement(Tip, {
|
|
119
|
+
key: "when",
|
|
120
|
+
className: "mk-when",
|
|
121
|
+
delayMs: TIME_DELAY_MS,
|
|
122
|
+
sentence: absoluteTime(comment.createdAt)
|
|
123
|
+
}, relativeTime(comment.createdAt, Date.now()))]);
|
|
124
|
+
}
|
|
125
|
+
function name(author) {
|
|
126
|
+
const slot = author.colorSlot ?? 0;
|
|
127
|
+
return createElement(Tip, {
|
|
128
|
+
key: "name",
|
|
129
|
+
className: "mk-name",
|
|
130
|
+
sentence: `${author.name} — ${PROVENANCE_SENTENCES[author.provenance]}`,
|
|
131
|
+
triggerProps: {
|
|
132
|
+
...dataAttributes({ provenance: author.provenance }),
|
|
133
|
+
ref: (node) => {
|
|
134
|
+
if (node) applyReviewerSlot(node, slot);
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}, author.name);
|
|
138
|
+
}
|
|
139
|
+
function meta(comment) {
|
|
140
|
+
const attachments = comment.attachments?.length ?? 0;
|
|
141
|
+
return renderPart("div", false, {
|
|
142
|
+
key: "meta",
|
|
143
|
+
className: "mk-meta"
|
|
144
|
+
}, [comment.status === "orphaned" ? null : place(comment), attachments > 0 ? createElement(Tip, {
|
|
145
|
+
key: "shot",
|
|
146
|
+
className: "mk-chip",
|
|
147
|
+
sentence: ISLAND_COPY.attachmentSentence
|
|
148
|
+
}, ISLAND_COPY.attachment) : null]);
|
|
149
|
+
}
|
|
150
|
+
function place(comment) {
|
|
151
|
+
const phrase = kindPhrase(kindOf(comment.anchor), labelFor({ anchor: comment.anchor }));
|
|
152
|
+
return renderPart("span", false, {
|
|
153
|
+
key: "on",
|
|
154
|
+
className: "mk-when"
|
|
155
|
+
}, ["on ", renderPart("b", false, { key: "b" }, phrase)]);
|
|
156
|
+
}
|
|
157
|
+
//#endregion
|
|
158
|
+
export { Item, TIME_DELAY_MS };
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { CommentFilter, Corner, PickKind, ThemePreference } from "@maple-kit/core/client";
|
|
2
|
+
//#region src/island/language.d.ts
|
|
3
|
+
/** The five filters, in the words the pills show. `unpinned` is `orphaned`. */
|
|
4
|
+
export declare const FILTER_LABELS: Readonly<Record<CommentFilter, string>>;
|
|
5
|
+
/** One sentence per setting: what it does, not what it is called again. */
|
|
6
|
+
export declare const SETTINGS_COPY: {
|
|
7
|
+
readonly theme: {
|
|
8
|
+
readonly name: "Theme";
|
|
9
|
+
readonly hint: "What the overlay itself is drawn in. A comment always records the page's own.";
|
|
10
|
+
};
|
|
11
|
+
readonly position: {
|
|
12
|
+
readonly name: "Corner";
|
|
13
|
+
readonly hint: "Where the island sits. Dragging it by its pill does the same thing.";
|
|
14
|
+
};
|
|
15
|
+
readonly hideResolved: {
|
|
16
|
+
readonly name: "Hide resolved";
|
|
17
|
+
readonly hint: "Done comments stay off the page and out of the list until you pick the Resolved filter.";
|
|
18
|
+
};
|
|
19
|
+
readonly developer: {
|
|
20
|
+
readonly name: "Developer mode";
|
|
21
|
+
readonly hint: "Shows how each comment is re-found after a deploy, its source line and its CSS path.";
|
|
22
|
+
};
|
|
23
|
+
readonly untagged: {
|
|
24
|
+
readonly name: "This build is not tagged";
|
|
25
|
+
readonly hint: string;
|
|
26
|
+
};
|
|
27
|
+
readonly hidden: {
|
|
28
|
+
readonly name: "Hide the island";
|
|
29
|
+
readonly hint: "It goes until you reload. A comment arriving, or a link to one, brings it straight back.";
|
|
30
|
+
};
|
|
31
|
+
};
|
|
32
|
+
/** Copy with no better home than a name. */
|
|
33
|
+
export declare const ISLAND_COPY: {
|
|
34
|
+
readonly title: "Comments";
|
|
35
|
+
readonly settings: "Settings";
|
|
36
|
+
readonly close: "Close the inventory";
|
|
37
|
+
readonly closeGlyph: "✕";
|
|
38
|
+
readonly newComment: "New:";
|
|
39
|
+
readonly empty: "Nothing here under this filter.";
|
|
40
|
+
readonly loading: "Reading the comments on this branch…";
|
|
41
|
+
readonly unread: "The comments could not be read, so this list is not the whole story.";
|
|
42
|
+
readonly showAll: "Show all";
|
|
43
|
+
readonly showLess: "Show less";
|
|
44
|
+
readonly hide: "Hide";
|
|
45
|
+
readonly attachment: "shot";
|
|
46
|
+
readonly attachmentSentence: "A screenshot went with this comment. The panel shows it.";
|
|
47
|
+
};
|
|
48
|
+
/** The pill's own words. One number, and it is the open one. */
|
|
49
|
+
export declare function openLabel(count: number): string;
|
|
50
|
+
/**
|
|
51
|
+
* What a comment is on, in the words a reviewer would use. A passage is in
|
|
52
|
+
* something and a region is an area of it; an element is simply its own name.
|
|
53
|
+
*/
|
|
54
|
+
export declare function kindPhrase(kind: PickKind, human: string | undefined): string;
|
|
55
|
+
//#endregion
|