@stll/ui 0.5.2 → 0.7.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 +71 -2
- package/dist/components/application-shell.d.ts +27 -0
- package/dist/components/application-shell.js +25 -0
- package/dist/components/button-variants.d.ts +2 -2
- package/dist/components/input-group.d.ts +1 -1
- package/dist/components/loader.d.ts +36 -0
- package/dist/components/loader.js +54 -0
- package/dist/index.d.ts +12 -1
- package/dist/index.js +12 -1
- package/dist/inspector/chrome.d.ts +6 -1
- package/dist/inspector/chrome.js +8 -3
- package/dist/inspector/layout-tokens.d.ts +1 -2
- package/dist/inspector/layout-tokens.js +1 -2
- package/dist/kanban/index.d.ts +3 -1
- package/dist/kanban/index.js +3 -1
- package/dist/kanban/sortable-edge.d.ts +39 -0
- package/dist/kanban/sortable-edge.js +27 -0
- package/dist/kanban/sortable-interactions.d.ts +75 -0
- package/dist/kanban/sortable-interactions.js +210 -0
- package/dist/kanban/touch-identity.d.ts +8 -0
- package/dist/kanban/touch-identity.js +4 -0
- package/dist/lib/initials.d.ts +16 -0
- package/dist/lib/initials.js +27 -0
- package/dist/review/review-author-avatar.d.ts +23 -0
- package/dist/review/review-author-avatar.js +30 -0
- package/dist/review/review-comment-card.d.ts +33 -0
- package/dist/review/review-comment-card.js +76 -0
- package/dist/review/review-decision-actions.d.ts +32 -0
- package/dist/review/review-decision-actions.js +74 -0
- package/dist/review/review-diff-text.d.ts +33 -0
- package/dist/review/review-diff-text.js +67 -0
- package/dist/review/review-out-of-date-notice.d.ts +28 -0
- package/dist/review/review-out-of-date-notice.js +32 -0
- package/dist/review/review-severity-dot.d.ts +27 -0
- package/dist/review/review-severity-dot.js +35 -0
- package/dist/review/review-status-badge.d.ts +30 -0
- package/dist/review/review-status-badge.js +45 -0
- package/dist/styles/theme.css +26 -0
- package/package.json +58 -5
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Button } from "../components/button.js";
|
|
2
|
+
import * as React$1 from "react";
|
|
3
|
+
import { AutoScrollOptions, CollisionDetection, DndContextProps, DragCancelEvent, DragEndEvent, DragOverlayProps, DragStartEvent, KeyboardCoordinateGetter, UniqueIdentifier } from "@dnd-kit/core";
|
|
4
|
+
import { SortableContextProps, useSortable } from "@dnd-kit/sortable";
|
|
5
|
+
//#region src/kanban/sortable-interactions.d.ts
|
|
6
|
+
declare const KANBAN_MOUSE_ACTIVATION_DISTANCE = 8;
|
|
7
|
+
declare const KANBAN_TOUCH_ACTIVATION_CONSTRAINT: {
|
|
8
|
+
readonly delay: 150;
|
|
9
|
+
readonly tolerance: 8;
|
|
10
|
+
};
|
|
11
|
+
type KanbanSortableBoardProps = {
|
|
12
|
+
children: React$1.ReactNode;
|
|
13
|
+
onDragEnd: (event: DragEndEvent) => void;
|
|
14
|
+
collisionDetection?: CollisionDetection | undefined;
|
|
15
|
+
keyboardCoordinates?: KeyboardCoordinateGetter | undefined;
|
|
16
|
+
autoScroll?: boolean | AutoScrollOptions | undefined;
|
|
17
|
+
/** Replaces the default mouse, touch, and keyboard sensors when supplied. */
|
|
18
|
+
sensors?: DndContextProps["sensors"] | undefined;
|
|
19
|
+
/** Overrides dnd-kit's screen-reader announcements when supplied. */
|
|
20
|
+
accessibility?: DndContextProps["accessibility"] | undefined;
|
|
21
|
+
onDragStart?: ((event: DragStartEvent) => void) | undefined;
|
|
22
|
+
onDragCancel?: ((event: DragCancelEvent) => void) | undefined;
|
|
23
|
+
/** Rendered in document.body while an item is active. */
|
|
24
|
+
overlay?: ((activeId: UniqueIdentifier | null) => React$1.ReactNode) | undefined;
|
|
25
|
+
overlayProps?: Omit<DragOverlayProps, "children"> | undefined;
|
|
26
|
+
};
|
|
27
|
+
/**
|
|
28
|
+
* Input-complete drag context for sortable boards.
|
|
29
|
+
*
|
|
30
|
+
* The consumer owns identifiers and the result of a drop; this component owns
|
|
31
|
+
* the sensor activation rules, keyboard navigation, auto-scroll configuration,
|
|
32
|
+
* and overlay lifecycle shared by sortable board UIs.
|
|
33
|
+
*/
|
|
34
|
+
declare const KanbanSortableBoard: ({ children, onDragEnd, collisionDetection, keyboardCoordinates, autoScroll, sensors, accessibility, onDragStart, onDragCancel, overlay, overlayProps }: KanbanSortableBoardProps) => React$1.JSX.Element;
|
|
35
|
+
declare const useKanbanSortableSensors: (keyboardCoordinates?: KeyboardCoordinateGetter) => import("@dnd-kit/core").SensorDescriptor<import("@dnd-kit/core").SensorOptions>[];
|
|
36
|
+
type KanbanSortableListProps = SortableContextProps & React$1.ComponentProps<"div">;
|
|
37
|
+
/** A vertical card list that preserves native vertical touch scrolling. */
|
|
38
|
+
declare const KanbanSortableList: ({ className, children, id, items, strategy, disabled, ...props }: KanbanSortableListProps) => React$1.JSX.Element;
|
|
39
|
+
type KanbanSortableColumnsProps = SortableContextProps & React$1.ComponentProps<"div">;
|
|
40
|
+
/** A horizontal column list that keeps the board pan gesture available. */
|
|
41
|
+
declare const KanbanSortableColumns: ({ className, children, id, items, strategy, disabled, ...props }: KanbanSortableColumnsProps) => React$1.JSX.Element;
|
|
42
|
+
type KanbanSortableBindings = Pick<ReturnType<typeof useSortable>, "attributes" | "listeners" | "setActivatorNodeRef">;
|
|
43
|
+
type UseKanbanSortableOptions = {
|
|
44
|
+
id: UniqueIdentifier;
|
|
45
|
+
disabled?: boolean | undefined;
|
|
46
|
+
};
|
|
47
|
+
/**
|
|
48
|
+
* Connect a sortable item and its separate drag handle without making the
|
|
49
|
+
* item's content a touch-none activation surface.
|
|
50
|
+
*/
|
|
51
|
+
declare const useKanbanSortable: ({ id, disabled }: UseKanbanSortableOptions) => {
|
|
52
|
+
isDragging: boolean;
|
|
53
|
+
setNodeRef: (node: HTMLElement | null) => void;
|
|
54
|
+
style: {
|
|
55
|
+
transform: string | undefined;
|
|
56
|
+
transition: string | undefined;
|
|
57
|
+
};
|
|
58
|
+
dragHandle: {
|
|
59
|
+
attributes: import("@dnd-kit/core").DraggableAttributes;
|
|
60
|
+
listeners: import("@dnd-kit/core/dist/hooks/utilities").SyntheticListenerMap | undefined;
|
|
61
|
+
setActivatorNodeRef: (element: HTMLElement | null) => void;
|
|
62
|
+
};
|
|
63
|
+
};
|
|
64
|
+
type KanbanDragHandleProps = {
|
|
65
|
+
bindings: KanbanSortableBindings;
|
|
66
|
+
label: string;
|
|
67
|
+
} & Omit<React$1.ComponentProps<typeof Button>, "aria-label" | "children" | "ref" | "tooltip" | "type">;
|
|
68
|
+
/**
|
|
69
|
+
* A 44px keyboard-accessible drag activator. The handle is the only board
|
|
70
|
+
* surface that disables touch panning, leaving cards and scroll regions free
|
|
71
|
+
* for ordinary scrolling.
|
|
72
|
+
*/
|
|
73
|
+
declare const KanbanDragHandle: ({ bindings, label, className, ...props }: KanbanDragHandleProps) => React$1.JSX.Element;
|
|
74
|
+
//#endregion
|
|
75
|
+
export { KANBAN_MOUSE_ACTIVATION_DISTANCE, KANBAN_TOUCH_ACTIVATION_CONSTRAINT, KanbanDragHandle, KanbanDragHandleProps, KanbanSortableBindings, KanbanSortableBoard, KanbanSortableBoardProps, KanbanSortableColumns, KanbanSortableColumnsProps, KanbanSortableList, KanbanSortableListProps, UseKanbanSortableOptions, useKanbanSortable, useKanbanSortableSensors };
|
|
@@ -0,0 +1,210 @@
|
|
|
1
|
+
"use client";
|
|
2
|
+
import { cn } from "../lib/utils.js";
|
|
3
|
+
import { Button } from "../components/button.js";
|
|
4
|
+
import { isActiveTouchChange } from "./touch-identity.js";
|
|
5
|
+
import { GripVerticalIcon } from "lucide-react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import * as React$1 from "react";
|
|
8
|
+
import { createPortal } from "react-dom";
|
|
9
|
+
import { DndContext, DragOverlay, KeyboardSensor, MouseSensor, TouchSensor, useSensor, useSensors } from "@dnd-kit/core";
|
|
10
|
+
import { SortableContext, sortableKeyboardCoordinates, useSortable } from "@dnd-kit/sortable";
|
|
11
|
+
//#region src/kanban/sortable-interactions.tsx
|
|
12
|
+
const KANBAN_MOUSE_ACTIVATION_DISTANCE = 8;
|
|
13
|
+
const KANBAN_TOUCH_ACTIVATION_CONSTRAINT = {
|
|
14
|
+
delay: 150,
|
|
15
|
+
tolerance: 8
|
|
16
|
+
};
|
|
17
|
+
const hasTouchLists = (event) => "changedTouches" in event && "touches" in event;
|
|
18
|
+
const getTouchIdentifier = (event) => {
|
|
19
|
+
if (!hasTouchLists(event)) return null;
|
|
20
|
+
return event.changedTouches.item(0)?.identifier ?? event.touches.item(0)?.identifier ?? null;
|
|
21
|
+
};
|
|
22
|
+
const getTouchIdentifiers = (touches) => {
|
|
23
|
+
const identifiers = [];
|
|
24
|
+
for (let index = 0; index < touches.length; index += 1) {
|
|
25
|
+
const identifier = touches.item(index)?.identifier;
|
|
26
|
+
if (identifier !== void 0) identifiers.push(identifier);
|
|
27
|
+
}
|
|
28
|
+
return identifiers;
|
|
29
|
+
};
|
|
30
|
+
/**
|
|
31
|
+
* Keeps a delayed touch drag bound to the finger that activated it.
|
|
32
|
+
*
|
|
33
|
+
* dnd-kit's stock touch sensor attaches document-level lifecycle listeners,
|
|
34
|
+
* so a second finger can otherwise move, end, or cancel the active drag. The
|
|
35
|
+
* capture listeners below suppress only secondary touch changes before those
|
|
36
|
+
* listeners receive them; browser scrolling remains native because no default
|
|
37
|
+
* action is prevented here.
|
|
38
|
+
*/
|
|
39
|
+
var KanbanTouchSensor = class extends TouchSensor {
|
|
40
|
+
identityListeners;
|
|
41
|
+
ownerDocument;
|
|
42
|
+
touchIdentifier;
|
|
43
|
+
constructor(props) {
|
|
44
|
+
const identityListeners = new AbortController();
|
|
45
|
+
super({
|
|
46
|
+
...props,
|
|
47
|
+
onAbort: (active) => {
|
|
48
|
+
identityListeners.abort();
|
|
49
|
+
props.onAbort(active);
|
|
50
|
+
},
|
|
51
|
+
onCancel: () => {
|
|
52
|
+
identityListeners.abort();
|
|
53
|
+
props.onCancel();
|
|
54
|
+
},
|
|
55
|
+
onEnd: () => {
|
|
56
|
+
identityListeners.abort();
|
|
57
|
+
props.onEnd();
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
this.identityListeners = identityListeners;
|
|
61
|
+
this.ownerDocument = getTouchDocument(props.event);
|
|
62
|
+
this.touchIdentifier = getTouchIdentifier(props.event);
|
|
63
|
+
this.ownerDocument.addEventListener("touchmove", this.handleTouchMove, {
|
|
64
|
+
capture: true,
|
|
65
|
+
passive: false,
|
|
66
|
+
signal: identityListeners.signal
|
|
67
|
+
});
|
|
68
|
+
this.ownerDocument.addEventListener("touchend", this.handleTouchEnd, {
|
|
69
|
+
capture: true,
|
|
70
|
+
signal: identityListeners.signal
|
|
71
|
+
});
|
|
72
|
+
this.ownerDocument.addEventListener("touchcancel", this.handleTouchCancel, {
|
|
73
|
+
capture: true,
|
|
74
|
+
signal: identityListeners.signal
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
handleTouchMove = (event) => {
|
|
78
|
+
if (this.isPrimaryTouchChange(event)) return;
|
|
79
|
+
event.stopImmediatePropagation();
|
|
80
|
+
};
|
|
81
|
+
handleTouchEnd = (event) => {
|
|
82
|
+
if (!this.isPrimaryTouchChange(event)) {
|
|
83
|
+
event.stopImmediatePropagation();
|
|
84
|
+
return;
|
|
85
|
+
}
|
|
86
|
+
this.detachIdentityListeners();
|
|
87
|
+
};
|
|
88
|
+
handleTouchCancel = (event) => {
|
|
89
|
+
if (!this.isPrimaryTouchChange(event)) {
|
|
90
|
+
event.stopImmediatePropagation();
|
|
91
|
+
return;
|
|
92
|
+
}
|
|
93
|
+
this.detachIdentityListeners();
|
|
94
|
+
};
|
|
95
|
+
isPrimaryTouchChange = (event) => isActiveTouchChange({
|
|
96
|
+
activeTouchIdentifier: this.touchIdentifier,
|
|
97
|
+
changedTouchIdentifiers: getTouchIdentifiers(event.changedTouches)
|
|
98
|
+
});
|
|
99
|
+
detachIdentityListeners = () => {
|
|
100
|
+
this.identityListeners.abort();
|
|
101
|
+
};
|
|
102
|
+
};
|
|
103
|
+
const getTouchDocument = (event) => {
|
|
104
|
+
if (event.target instanceof Node && event.target.ownerDocument) return event.target.ownerDocument;
|
|
105
|
+
return document;
|
|
106
|
+
};
|
|
107
|
+
/**
|
|
108
|
+
* Input-complete drag context for sortable boards.
|
|
109
|
+
*
|
|
110
|
+
* The consumer owns identifiers and the result of a drop; this component owns
|
|
111
|
+
* the sensor activation rules, keyboard navigation, auto-scroll configuration,
|
|
112
|
+
* and overlay lifecycle shared by sortable board UIs.
|
|
113
|
+
*/
|
|
114
|
+
const KanbanSortableBoard = ({ children, onDragEnd, collisionDetection, keyboardCoordinates, autoScroll, sensors, accessibility, onDragStart, onDragCancel, overlay, overlayProps }) => {
|
|
115
|
+
const [activeId, setActiveId] = React$1.useState(null);
|
|
116
|
+
const defaultSensors = useKanbanSortableSensors(keyboardCoordinates);
|
|
117
|
+
const handleDragStart = (event) => {
|
|
118
|
+
setActiveId(event.active.id);
|
|
119
|
+
onDragStart?.(event);
|
|
120
|
+
};
|
|
121
|
+
const handleDragEnd = (event) => {
|
|
122
|
+
setActiveId(null);
|
|
123
|
+
onDragEnd(event);
|
|
124
|
+
};
|
|
125
|
+
const handleDragCancel = (event) => {
|
|
126
|
+
setActiveId(null);
|
|
127
|
+
onDragCancel?.(event);
|
|
128
|
+
};
|
|
129
|
+
return /* @__PURE__ */ jsxs(DndContext, {
|
|
130
|
+
...autoScroll === void 0 ? {} : { autoScroll },
|
|
131
|
+
...accessibility === void 0 ? {} : { accessibility },
|
|
132
|
+
...collisionDetection === void 0 ? {} : { collisionDetection },
|
|
133
|
+
onDragCancel: handleDragCancel,
|
|
134
|
+
onDragEnd: handleDragEnd,
|
|
135
|
+
onDragStart: handleDragStart,
|
|
136
|
+
sensors: sensors ?? defaultSensors,
|
|
137
|
+
children: [children, overlay && typeof document !== "undefined" ? createPortal(/* @__PURE__ */ jsx(DragOverlay, {
|
|
138
|
+
...overlayProps,
|
|
139
|
+
children: overlay(activeId)
|
|
140
|
+
}), document.body) : null]
|
|
141
|
+
});
|
|
142
|
+
};
|
|
143
|
+
const useKanbanSortableSensors = (keyboardCoordinates = sortableKeyboardCoordinates) => useSensors(useSensor(MouseSensor, { activationConstraint: { distance: 8 } }), useSensor(KanbanTouchSensor, { activationConstraint: KANBAN_TOUCH_ACTIVATION_CONSTRAINT }), useSensor(KeyboardSensor, { coordinateGetter: keyboardCoordinates }));
|
|
144
|
+
/** A vertical card list that preserves native vertical touch scrolling. */
|
|
145
|
+
const KanbanSortableList = ({ className, children, id, items, strategy, disabled, ...props }) => /* @__PURE__ */ jsx(SortableContext, {
|
|
146
|
+
...disabled === void 0 ? {} : { disabled },
|
|
147
|
+
...id === void 0 ? {} : { id },
|
|
148
|
+
items,
|
|
149
|
+
...strategy === void 0 ? {} : { strategy },
|
|
150
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
151
|
+
className: cn("min-h-0 touch-auto overflow-y-auto overscroll-y-contain", className),
|
|
152
|
+
...props,
|
|
153
|
+
children
|
|
154
|
+
})
|
|
155
|
+
});
|
|
156
|
+
/** A horizontal column list that keeps the board pan gesture available. */
|
|
157
|
+
const KanbanSortableColumns = ({ className, children, id, items, strategy, disabled, ...props }) => /* @__PURE__ */ jsx(SortableContext, {
|
|
158
|
+
...disabled === void 0 ? {} : { disabled },
|
|
159
|
+
...id === void 0 ? {} : { id },
|
|
160
|
+
items,
|
|
161
|
+
...strategy === void 0 ? {} : { strategy },
|
|
162
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
163
|
+
className: cn("flex min-h-0 touch-auto overflow-x-auto overscroll-x-contain", className),
|
|
164
|
+
...props,
|
|
165
|
+
children
|
|
166
|
+
})
|
|
167
|
+
});
|
|
168
|
+
/**
|
|
169
|
+
* Connect a sortable item and its separate drag handle without making the
|
|
170
|
+
* item's content a touch-none activation surface.
|
|
171
|
+
*/
|
|
172
|
+
const useKanbanSortable = ({ id, disabled }) => {
|
|
173
|
+
const sortable = useSortable({
|
|
174
|
+
id,
|
|
175
|
+
...disabled === void 0 ? {} : { disabled }
|
|
176
|
+
});
|
|
177
|
+
return {
|
|
178
|
+
isDragging: sortable.isDragging,
|
|
179
|
+
setNodeRef: sortable.setNodeRef,
|
|
180
|
+
style: {
|
|
181
|
+
transform: sortable.transform ? `translate3d(${sortable.transform.x}px, ${sortable.transform.y}px, 0)` : void 0,
|
|
182
|
+
transition: sortable.transition
|
|
183
|
+
},
|
|
184
|
+
dragHandle: {
|
|
185
|
+
attributes: sortable.attributes,
|
|
186
|
+
listeners: sortable.listeners,
|
|
187
|
+
setActivatorNodeRef: sortable.setActivatorNodeRef
|
|
188
|
+
}
|
|
189
|
+
};
|
|
190
|
+
};
|
|
191
|
+
/**
|
|
192
|
+
* A 44px keyboard-accessible drag activator. The handle is the only board
|
|
193
|
+
* surface that disables touch panning, leaving cards and scroll regions free
|
|
194
|
+
* for ordinary scrolling.
|
|
195
|
+
*/
|
|
196
|
+
const KanbanDragHandle = ({ bindings, label, className, ...props }) => /* @__PURE__ */ jsx(Button, {
|
|
197
|
+
...props,
|
|
198
|
+
...bindings.attributes,
|
|
199
|
+
...bindings.listeners,
|
|
200
|
+
"aria-label": label,
|
|
201
|
+
className: cn("size-11 touch-none sm:size-11", className),
|
|
202
|
+
ref: (element) => bindings.setActivatorNodeRef(element),
|
|
203
|
+
size: "icon-xl",
|
|
204
|
+
tooltip: false,
|
|
205
|
+
type: "button",
|
|
206
|
+
variant: "ghost",
|
|
207
|
+
children: /* @__PURE__ */ jsx(GripVerticalIcon, { "aria-hidden": "true" })
|
|
208
|
+
});
|
|
209
|
+
//#endregion
|
|
210
|
+
export { KANBAN_MOUSE_ACTIVATION_DISTANCE, KANBAN_TOUCH_ACTIVATION_CONSTRAINT, KanbanDragHandle, KanbanSortableBoard, KanbanSortableColumns, KanbanSortableList, useKanbanSortable, useKanbanSortableSensors };
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
//#region src/kanban/touch-identity.d.ts
|
|
2
|
+
type ActiveTouchChangeOptions = {
|
|
3
|
+
activeTouchIdentifier: number | null;
|
|
4
|
+
changedTouchIdentifiers: readonly number[];
|
|
5
|
+
};
|
|
6
|
+
declare const isActiveTouchChange: ({ activeTouchIdentifier, changedTouchIdentifiers }: ActiveTouchChangeOptions) => boolean;
|
|
7
|
+
//#endregion
|
|
8
|
+
export { isActiveTouchChange };
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
//#region src/lib/initials.d.ts
|
|
2
|
+
/**
|
|
3
|
+
* Extract up to two uppercase initials from a display name.
|
|
4
|
+
*
|
|
5
|
+
* Behaviour:
|
|
6
|
+
* - Multi-word: first letter of each of the first two words.
|
|
7
|
+
* "Eva Schmidt" → "ES", "Jan van Houten" → "JV"
|
|
8
|
+
* - Single-word (Latin): first two characters.
|
|
9
|
+
* "John" → "JO"
|
|
10
|
+
* - CJK / no-space scripts: first two characters.
|
|
11
|
+
* "王小明" → "王小"
|
|
12
|
+
* - Null / empty: "?"
|
|
13
|
+
*/
|
|
14
|
+
declare const getInitials: (name: string | null) => string;
|
|
15
|
+
//#endregion
|
|
16
|
+
export { getInitials };
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
//#region src/lib/initials.ts
|
|
2
|
+
/**
|
|
3
|
+
* Extract up to two uppercase initials from a display name.
|
|
4
|
+
*
|
|
5
|
+
* Behaviour:
|
|
6
|
+
* - Multi-word: first letter of each of the first two words.
|
|
7
|
+
* "Eva Schmidt" → "ES", "Jan van Houten" → "JV"
|
|
8
|
+
* - Single-word (Latin): first two characters.
|
|
9
|
+
* "John" → "JO"
|
|
10
|
+
* - CJK / no-space scripts: first two characters.
|
|
11
|
+
* "王小明" → "王小"
|
|
12
|
+
* - Null / empty: "?"
|
|
13
|
+
*/
|
|
14
|
+
const getInitials = (name) => {
|
|
15
|
+
if (!name) return "?";
|
|
16
|
+
const trimmed = name.trim();
|
|
17
|
+
if (trimmed.length === 0) return "?";
|
|
18
|
+
const parts = trimmed.split(/\s+/u);
|
|
19
|
+
if (parts.length >= 2) {
|
|
20
|
+
const a = parts.at(0) ?? "";
|
|
21
|
+
const b = parts.at(1) ?? "";
|
|
22
|
+
return `${a.at(0) ?? ""}${b.at(0) ?? ""}`.toUpperCase();
|
|
23
|
+
}
|
|
24
|
+
return trimmed.slice(0, 2).toUpperCase();
|
|
25
|
+
};
|
|
26
|
+
//#endregion
|
|
27
|
+
export { getInitials };
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import { Avatar } from "../components/avatar.js";
|
|
2
|
+
import { ComponentProps } from "react";
|
|
3
|
+
//#region src/review/review-author-avatar.d.ts
|
|
4
|
+
/**
|
|
5
|
+
* Shown when an author has neither a name nor anything to fall back on. This
|
|
6
|
+
* package carries no catalogs, so a localized host passes `fallbackLabel`;
|
|
7
|
+
* the constant is exported so every surface that needs the same placeholder
|
|
8
|
+
* outside an avatar reads it from here rather than minting its own.
|
|
9
|
+
*/
|
|
10
|
+
declare const UNKNOWN_AUTHOR_LABEL = "Unknown user";
|
|
11
|
+
type ReviewAuthorAvatarProps = Omit<ComponentProps<typeof Avatar>, "children" | "className"> & {
|
|
12
|
+
image?: string | null | undefined;
|
|
13
|
+
name?: string | null;
|
|
14
|
+
deleted?: boolean | undefined;
|
|
15
|
+
fallbackLabel?: string;
|
|
16
|
+
className?: string | undefined;
|
|
17
|
+
fallbackClassName?: string | undefined;
|
|
18
|
+
};
|
|
19
|
+
/** The author's face on any review surface: image when there is one, initials
|
|
20
|
+
* otherwise, dimmed once the account is gone. */
|
|
21
|
+
declare const ReviewAuthorAvatar: ({ deleted, image, name, fallbackLabel, className, fallbackClassName, ...avatarProps }: ReviewAuthorAvatarProps) => import("react").JSX.Element;
|
|
22
|
+
//#endregion
|
|
23
|
+
export { ReviewAuthorAvatar, UNKNOWN_AUTHOR_LABEL };
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
import { cn } from "../lib/utils.js";
|
|
2
|
+
import { Avatar, AvatarFallback, AvatarImage } from "../components/avatar.js";
|
|
3
|
+
import { getInitials } from "../lib/initials.js";
|
|
4
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/review/review-author-avatar.tsx
|
|
6
|
+
/**
|
|
7
|
+
* Shown when an author has neither a name nor anything to fall back on. This
|
|
8
|
+
* package carries no catalogs, so a localized host passes `fallbackLabel`;
|
|
9
|
+
* the constant is exported so every surface that needs the same placeholder
|
|
10
|
+
* outside an avatar reads it from here rather than minting its own.
|
|
11
|
+
*/
|
|
12
|
+
const UNKNOWN_AUTHOR_LABEL = "Unknown user";
|
|
13
|
+
/** The author's face on any review surface: image when there is one, initials
|
|
14
|
+
* otherwise, dimmed once the account is gone. */
|
|
15
|
+
const ReviewAuthorAvatar = ({ deleted = false, image, name, fallbackLabel = UNKNOWN_AUTHOR_LABEL, className, fallbackClassName, ...avatarProps }) => {
|
|
16
|
+
const displayName = name?.trim() || fallbackLabel;
|
|
17
|
+
return /* @__PURE__ */ jsxs(Avatar, {
|
|
18
|
+
...avatarProps,
|
|
19
|
+
className: cn(className, deleted && "opacity-60 grayscale"),
|
|
20
|
+
children: [image ? /* @__PURE__ */ jsx(AvatarImage, {
|
|
21
|
+
alt: displayName,
|
|
22
|
+
src: image
|
|
23
|
+
}) : null, /* @__PURE__ */ jsx(AvatarFallback, {
|
|
24
|
+
className: cn(fallbackClassName, deleted && "bg-muted text-muted-foreground"),
|
|
25
|
+
children: getInitials(name ?? null)
|
|
26
|
+
})]
|
|
27
|
+
});
|
|
28
|
+
};
|
|
29
|
+
//#endregion
|
|
30
|
+
export { ReviewAuthorAvatar, UNKNOWN_AUTHOR_LABEL };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/review/review-comment-card.d.ts
|
|
3
|
+
type ReviewCommentAuthor = {
|
|
4
|
+
name: string | null;
|
|
5
|
+
image?: string | null | undefined;
|
|
6
|
+
deleted?: boolean | undefined;
|
|
7
|
+
};
|
|
8
|
+
type ReviewCommentCardProps = {
|
|
9
|
+
author: ReviewCommentAuthor;
|
|
10
|
+
/** Machine-readable instant for `<time>`; ISO string or Date. */
|
|
11
|
+
timestamp: string | Date;
|
|
12
|
+
/** The same instant, already formatted by the host's locale formatter. */
|
|
13
|
+
formattedTime: string;
|
|
14
|
+
body: ReactNode;
|
|
15
|
+
/** The text the comment was written against, when it is worth echoing. */
|
|
16
|
+
anchorText?: string | undefined;
|
|
17
|
+
/** The comment is anchored to text the document has since moved past. */
|
|
18
|
+
isStale?: boolean | undefined;
|
|
19
|
+
staleLabel?: string | undefined;
|
|
20
|
+
resolved?: boolean | undefined;
|
|
21
|
+
onToggleResolved?: (() => void) | undefined;
|
|
22
|
+
resolveLabel?: string | undefined;
|
|
23
|
+
reopenLabel?: string | undefined;
|
|
24
|
+
canDelete?: boolean | undefined;
|
|
25
|
+
onDelete?: (() => void) | undefined;
|
|
26
|
+
deleteLabel?: string | undefined;
|
|
27
|
+
className?: string;
|
|
28
|
+
};
|
|
29
|
+
/** One comment on a reviewed surface: who wrote it, when, what it says, what
|
|
30
|
+
* it points at, and the two things a reader can do to it. */
|
|
31
|
+
declare const ReviewCommentCard: ({ author, timestamp, formattedTime, body, anchorText, isStale, staleLabel, resolved, onToggleResolved, resolveLabel, reopenLabel, canDelete, onDelete, deleteLabel, className }: ReviewCommentCardProps) => import("react").JSX.Element;
|
|
32
|
+
//#endregion
|
|
33
|
+
export { ReviewCommentAuthor, ReviewCommentCard };
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { cn } from "../lib/utils.js";
|
|
2
|
+
import { BidiText } from "../components/bidi-text.js";
|
|
3
|
+
import { Button } from "../components/button.js";
|
|
4
|
+
import { ReviewAuthorAvatar } from "./review-author-avatar.js";
|
|
5
|
+
import { CheckIcon, RotateCcwIcon, Trash2Icon } from "lucide-react";
|
|
6
|
+
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
//#region src/review/review-comment-card.tsx
|
|
8
|
+
/** One comment on a reviewed surface: who wrote it, when, what it says, what
|
|
9
|
+
* it points at, and the two things a reader can do to it. */
|
|
10
|
+
const ReviewCommentCard = ({ author, timestamp, formattedTime, body, anchorText, isStale = false, staleLabel, resolved = false, onToggleResolved, resolveLabel, reopenLabel, canDelete = false, onDelete, deleteLabel, className }) => /* @__PURE__ */ jsxs("article", {
|
|
11
|
+
className: cn("flex items-start gap-2 px-3 py-2", resolved && "opacity-60", className),
|
|
12
|
+
"data-slot": "review-comment-card",
|
|
13
|
+
children: [
|
|
14
|
+
/* @__PURE__ */ jsx(ReviewAuthorAvatar, {
|
|
15
|
+
className: "mt-0.5 size-5 shrink-0 text-[9px]",
|
|
16
|
+
deleted: author.deleted,
|
|
17
|
+
image: author.image,
|
|
18
|
+
name: author.name
|
|
19
|
+
}),
|
|
20
|
+
/* @__PURE__ */ jsxs("div", {
|
|
21
|
+
className: "min-w-0 flex-1",
|
|
22
|
+
children: [
|
|
23
|
+
/* @__PURE__ */ jsxs("p", {
|
|
24
|
+
className: "text-muted-foreground flex min-w-0 items-baseline gap-1.5 text-[11px]",
|
|
25
|
+
children: [/* @__PURE__ */ jsx(BidiText, {
|
|
26
|
+
as: "span",
|
|
27
|
+
className: "text-foreground-strong-muted truncate font-medium",
|
|
28
|
+
children: author.name
|
|
29
|
+
}), /* @__PURE__ */ jsx("time", {
|
|
30
|
+
className: "shrink-0 tabular-nums",
|
|
31
|
+
dateTime: toIsoInstant(timestamp),
|
|
32
|
+
children: formattedTime
|
|
33
|
+
})]
|
|
34
|
+
}),
|
|
35
|
+
/* @__PURE__ */ jsx(BidiText, {
|
|
36
|
+
as: "div",
|
|
37
|
+
className: "text-foreground text-xs wrap-anywhere",
|
|
38
|
+
children: body
|
|
39
|
+
}),
|
|
40
|
+
anchorText === void 0 || anchorText === "" ? null : /* @__PURE__ */ jsx(BidiText, {
|
|
41
|
+
as: "p",
|
|
42
|
+
className: "text-muted-foreground mt-0.5 truncate text-[11px] italic",
|
|
43
|
+
children: anchorText
|
|
44
|
+
}),
|
|
45
|
+
isStale && staleLabel !== void 0 ? /* @__PURE__ */ jsx("p", {
|
|
46
|
+
className: "text-muted-foreground mt-0.5 text-[11px]",
|
|
47
|
+
children: staleLabel
|
|
48
|
+
}) : null
|
|
49
|
+
]
|
|
50
|
+
}),
|
|
51
|
+
/* @__PURE__ */ jsxs("div", {
|
|
52
|
+
className: "flex shrink-0 items-center gap-0.5",
|
|
53
|
+
children: [onToggleResolved === void 0 ? null : /* @__PURE__ */ jsx(Button, {
|
|
54
|
+
"aria-label": resolved ? reopenLabel : resolveLabel,
|
|
55
|
+
onClick: onToggleResolved,
|
|
56
|
+
size: "icon-xs",
|
|
57
|
+
variant: "ghost",
|
|
58
|
+
children: resolved ? /* @__PURE__ */ jsx(RotateCcwIcon, {}) : /* @__PURE__ */ jsx(CheckIcon, {})
|
|
59
|
+
}), canDelete && onDelete !== void 0 ? /* @__PURE__ */ jsx(Button, {
|
|
60
|
+
"aria-label": deleteLabel,
|
|
61
|
+
onClick: onDelete,
|
|
62
|
+
size: "icon-xs",
|
|
63
|
+
variant: "ghost",
|
|
64
|
+
children: /* @__PURE__ */ jsx(Trash2Icon, {})
|
|
65
|
+
}) : null]
|
|
66
|
+
})
|
|
67
|
+
]
|
|
68
|
+
});
|
|
69
|
+
/** `<time dateTime>` wants a machine-readable instant. An unparseable date
|
|
70
|
+
* yields no attribute rather than throwing on `toISOString`. */
|
|
71
|
+
const toIsoInstant = (timestamp) => {
|
|
72
|
+
if (typeof timestamp === "string") return timestamp;
|
|
73
|
+
return Number.isNaN(timestamp.getTime()) ? void 0 : timestamp.toISOString();
|
|
74
|
+
};
|
|
75
|
+
//#endregion
|
|
76
|
+
export { ReviewCommentCard };
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
import { ReactNode } from "react";
|
|
2
|
+
//#region src/review/review-decision-actions.d.ts
|
|
3
|
+
/**
|
|
4
|
+
* Where a reviewable item stands. Undecided items offer accept/reject;
|
|
5
|
+
* decided ones offer the way back (revert the applied change, or reopen the
|
|
6
|
+
* decision). `applying` is undecided but in flight: the same pair, inert,
|
|
7
|
+
* so the row does not resize while the write lands.
|
|
8
|
+
*/
|
|
9
|
+
type ReviewDecisionState = "pending" | "accepted" | "rejected" | "dismissed" | "applying";
|
|
10
|
+
type ReviewDecisionSize = "xs" | "sm";
|
|
11
|
+
type ReviewDecisionActionsProps = {
|
|
12
|
+
state: ReviewDecisionState;
|
|
13
|
+
onAccept?: (() => void) | undefined;
|
|
14
|
+
onReject?: (() => void) | undefined;
|
|
15
|
+
onRevert?: (() => void) | undefined;
|
|
16
|
+
onReopen?: (() => void) | undefined;
|
|
17
|
+
size?: ReviewDecisionSize;
|
|
18
|
+
/** Labels are supplied by the host: this package carries no catalogs. */
|
|
19
|
+
acceptLabel?: ReactNode;
|
|
20
|
+
rejectLabel?: ReactNode;
|
|
21
|
+
revertLabel?: ReactNode;
|
|
22
|
+
/** Accessible name for the icon-only reopen control. */
|
|
23
|
+
reopenLabel?: string | undefined;
|
|
24
|
+
acceptTooltip?: ReactNode;
|
|
25
|
+
rejectTooltip?: ReactNode;
|
|
26
|
+
disabled?: boolean;
|
|
27
|
+
className?: string;
|
|
28
|
+
};
|
|
29
|
+
/** Accept / reject / revert / reopen, in one shape, for every review surface. */
|
|
30
|
+
declare const ReviewDecisionActions: ({ state, onAccept, onReject, onRevert, onReopen, size, acceptLabel, rejectLabel, revertLabel, reopenLabel, acceptTooltip, rejectTooltip, disabled, className }: ReviewDecisionActionsProps) => import("react").JSX.Element | null;
|
|
31
|
+
//#endregion
|
|
32
|
+
export { ReviewDecisionActions, ReviewDecisionSize, ReviewDecisionState };
|
|
@@ -0,0 +1,74 @@
|
|
|
1
|
+
import { cn } from "../lib/utils.js";
|
|
2
|
+
import { Button } from "../components/button.js";
|
|
3
|
+
import { CheckIcon, RotateCcwIcon, XIcon } from "lucide-react";
|
|
4
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
//#region src/review/review-decision-actions.tsx
|
|
6
|
+
/** Accept / reject / revert / reopen, in one shape, for every review surface. */
|
|
7
|
+
const ReviewDecisionActions = ({ state, onAccept, onReject, onRevert, onReopen, size = "sm", acceptLabel, rejectLabel, revertLabel, reopenLabel, acceptTooltip, rejectTooltip, disabled = false, className }) => {
|
|
8
|
+
const controls = renderControls({
|
|
9
|
+
acceptLabel,
|
|
10
|
+
acceptTooltip,
|
|
11
|
+
disabled,
|
|
12
|
+
onAccept,
|
|
13
|
+
onReject,
|
|
14
|
+
onReopen,
|
|
15
|
+
onRevert,
|
|
16
|
+
rejectLabel,
|
|
17
|
+
rejectTooltip,
|
|
18
|
+
reopenLabel,
|
|
19
|
+
revertLabel,
|
|
20
|
+
size,
|
|
21
|
+
state
|
|
22
|
+
});
|
|
23
|
+
if (controls === null) return null;
|
|
24
|
+
return /* @__PURE__ */ jsx("div", {
|
|
25
|
+
className: cn("flex items-center gap-1.5", className),
|
|
26
|
+
"data-slot": "review-decision-actions",
|
|
27
|
+
children: controls
|
|
28
|
+
});
|
|
29
|
+
};
|
|
30
|
+
const renderControls = ({ acceptLabel, acceptTooltip, disabled, onAccept, onReject, onReopen, onRevert, rejectLabel, rejectTooltip, reopenLabel, revertLabel, size, state }) => {
|
|
31
|
+
switch (state) {
|
|
32
|
+
case "pending":
|
|
33
|
+
case "applying": {
|
|
34
|
+
const inert = disabled || state === "applying";
|
|
35
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [onAccept === void 0 ? null : /* @__PURE__ */ jsxs(Button, {
|
|
36
|
+
disabled: inert,
|
|
37
|
+
onClick: onAccept,
|
|
38
|
+
size,
|
|
39
|
+
tooltip: acceptTooltip,
|
|
40
|
+
variant: "default",
|
|
41
|
+
children: [/* @__PURE__ */ jsx(CheckIcon, {}), acceptLabel]
|
|
42
|
+
}), onReject === void 0 ? null : /* @__PURE__ */ jsxs(Button, {
|
|
43
|
+
disabled: inert,
|
|
44
|
+
onClick: onReject,
|
|
45
|
+
size,
|
|
46
|
+
tooltip: rejectTooltip,
|
|
47
|
+
variant: "outline",
|
|
48
|
+
children: [/* @__PURE__ */ jsx(XIcon, {}), rejectLabel]
|
|
49
|
+
})] });
|
|
50
|
+
}
|
|
51
|
+
case "accepted":
|
|
52
|
+
case "rejected":
|
|
53
|
+
case "dismissed":
|
|
54
|
+
if (onRevert === void 0 && onReopen === void 0) return null;
|
|
55
|
+
return /* @__PURE__ */ jsxs(Fragment, { children: [onRevert === void 0 ? null : /* @__PURE__ */ jsx(Button, {
|
|
56
|
+
className: "text-muted-foreground hover:text-foreground",
|
|
57
|
+
disabled,
|
|
58
|
+
onClick: onRevert,
|
|
59
|
+
size,
|
|
60
|
+
variant: "ghost",
|
|
61
|
+
children: revertLabel
|
|
62
|
+
}), onReopen === void 0 ? null : /* @__PURE__ */ jsx(Button, {
|
|
63
|
+
"aria-label": reopenLabel,
|
|
64
|
+
disabled,
|
|
65
|
+
onClick: onReopen,
|
|
66
|
+
size: size === "xs" ? "icon-xs" : "icon-sm",
|
|
67
|
+
variant: "ghost",
|
|
68
|
+
children: /* @__PURE__ */ jsx(RotateCcwIcon, {})
|
|
69
|
+
})] });
|
|
70
|
+
default: return null;
|
|
71
|
+
}
|
|
72
|
+
};
|
|
73
|
+
//#endregion
|
|
74
|
+
export { ReviewDecisionActions };
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import { CSSProperties, ReactNode } from "react";
|
|
2
|
+
//#region src/review/review-diff-text.d.ts
|
|
3
|
+
declare const TRACKED_DELETION_STYLE: CSSProperties;
|
|
4
|
+
declare const TRACKED_INSERTION_STYLE: CSSProperties;
|
|
5
|
+
type ReviewDiffSegmentType = "equal" | "insert" | "delete";
|
|
6
|
+
type ReviewDiffSegment = {
|
|
7
|
+
type: ReviewDiffSegmentType;
|
|
8
|
+
text: string;
|
|
9
|
+
};
|
|
10
|
+
type ReviewDiffProps = {
|
|
11
|
+
className?: string;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
};
|
|
14
|
+
/** Inserted text, wherever it is shown. */
|
|
15
|
+
declare const ReviewDiffInsertion: ({ className, children }: ReviewDiffProps) => import("react").JSX.Element;
|
|
16
|
+
/** Deleted text, wherever it is shown. */
|
|
17
|
+
declare const ReviewDiffDeletion: ({ className, children }: ReviewDiffProps) => import("react").JSX.Element;
|
|
18
|
+
type ReviewDiffTextProps = {
|
|
19
|
+
segments: readonly ReviewDiffSegment[];
|
|
20
|
+
className?: string;
|
|
21
|
+
};
|
|
22
|
+
/** A word-level diff rendered inline, in the product's one track-changes
|
|
23
|
+
* language. */
|
|
24
|
+
declare const ReviewDiffText: ({ segments, className }: ReviewDiffTextProps) => import("react").JSX.Element;
|
|
25
|
+
/**
|
|
26
|
+
* A stable render key per segment. Type and text alone repeat inside a single
|
|
27
|
+
* diff (the same word deleted twice), so each repetition carries its
|
|
28
|
+
* occurrence count; the result is stable across renders of the same diff and
|
|
29
|
+
* never falls back to the array index.
|
|
30
|
+
*/
|
|
31
|
+
declare const reviewDiffSegmentKeys: (segments: readonly ReviewDiffSegment[]) => string[];
|
|
32
|
+
//#endregion
|
|
33
|
+
export { ReviewDiffDeletion, ReviewDiffInsertion, ReviewDiffSegment, ReviewDiffSegmentType, ReviewDiffText, TRACKED_DELETION_STYLE, TRACKED_INSERTION_STYLE, reviewDiffSegmentKeys };
|