@ciridae/ui 0.9.0 → 0.11.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/dist/components/blocks/comment-composer.js +20 -2
- package/dist/components/blocks/comment-thread.d.ts +1 -1
- package/dist/components/blocks/comment-thread.js +32 -2
- package/dist/components/blocks/feedback-date-format.d.ts +2 -2
- package/dist/components/blocks/feedback-date-format.js +2 -2
- package/dist/components/blocks/feedback-detail-sheet.js +4 -4
- package/dist/components/blocks/feedback-tracking-types.d.ts +7 -1
- package/dist/components/blocks/feedback-triage-timeline.d.ts +11 -0
- package/dist/components/blocks/feedback-triage-timeline.js +17 -0
- package/dist/components/blocks/feedback-type-badge.d.ts +6 -1
- package/dist/components/blocks/feedback-type-badge.js +12 -6
- package/dist/components/blocks/feedback-unread-badge.d.ts +6 -0
- package/dist/components/blocks/feedback-unread-badge.js +9 -0
- package/dist/components/blocks/feedback-user-detail.js +1 -1
- package/dist/components/blocks/feedback-user-list.js +1 -1
- package/dist/components/blocks/mcp-access-grouped-tool-picker.d.ts +19 -0
- package/dist/components/blocks/mcp-access-grouped-tool-picker.js +116 -0
- package/dist/components/blocks/mcp-access-tool-picker-internals.d.ts +35 -0
- package/dist/components/blocks/mcp-access-tool-picker-internals.js +24 -0
- package/dist/components/blocks/mcp-access-tool-picker.d.ts +4 -17
- package/dist/components/blocks/mcp-access-tool-picker.js +4 -12
- package/dist/components/blocks/mcp-access-types.d.ts +2 -0
- package/dist/components/blocks/timestamp-format.d.ts +7 -1
- package/dist/components/blocks/timestamp-format.js +9 -1
- package/dist/components/ui/checkbox.js +2 -2
- package/dist/components/ui/data-table-facet-filter.d.ts +22 -6
- package/dist/components/ui/data-table-facet-filter.js +31 -19
- package/dist/components/ui/data-table-search-input.d.ts +14 -0
- package/dist/components/ui/data-table-search-input.js +15 -0
- package/dist/components/ui/item.js +7 -3
- package/dist/entries/data-table-search-input.d.ts +1 -0
- package/dist/entries/data-table-search-input.js +1 -0
- package/dist/entries/feedback-triage-timeline.d.ts +1 -0
- package/dist/entries/feedback-triage-timeline.js +1 -0
- package/dist/entries/feedback-unread-badge.d.ts +1 -0
- package/dist/entries/feedback-unread-badge.js +1 -0
- package/dist/entries/mcp-access-grouped-tool-picker.d.ts +1 -0
- package/dist/entries/mcp-access-grouped-tool-picker.js +1 -0
- package/package.json +1 -1
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
2
|
import * as React from "react";
|
|
3
3
|
import { getCommentAuthorInitials } from "./comment-types";
|
|
4
|
+
import { cn } from "../../lib/utils";
|
|
4
5
|
import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
|
5
6
|
import { InputGroupButton, InputGroupTextarea } from "../ui/input-group";
|
|
6
7
|
const COMMENT_TEXTAREA_MAX_HEIGHT = 128;
|
|
@@ -17,9 +18,17 @@ function CommentComposer({ actionsSlot, author, defaultExpanded = false, default
|
|
|
17
18
|
});
|
|
18
19
|
React.useEffect(() => {
|
|
19
20
|
resizeCommentTextarea(textareaRef.current);
|
|
20
|
-
|
|
21
|
+
// showActions is a dep because each min-height bakes into the inline
|
|
22
|
+
// height this writes; a collapse without a value change must recompute.
|
|
23
|
+
}, [state.showActions, state.value]);
|
|
21
24
|
const isDisabled = disabled || isSubmitting;
|
|
22
|
-
return (_jsx("form", { onSubmit: state.onSubmit, children: _jsxs("div", { className: "grid w-full gap-1.5 rounded-md border border-input bg-background px-2.5 py-2 shadow-xs transition-[color,box-shadow] focus-within:border-ring focus-within:ring-[var(--focus-ring-width)] focus-within:ring-ring/50 dark:bg-input/30", "data-disabled": isDisabled ? "true" : undefined, children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [_jsx(CommentComposerAvatar, { author: author }), _jsx(InputGroupTextarea, { "aria-label": "Comment",
|
|
25
|
+
return (_jsx("form", { onSubmit: state.onSubmit, children: _jsxs("div", { className: "grid w-full gap-1.5 rounded-md border border-input bg-background px-2.5 py-2 shadow-xs transition-[color,box-shadow] focus-within:border-ring focus-within:ring-[var(--focus-ring-width)] focus-within:ring-ring/50 dark:bg-input/30", "data-disabled": isDisabled ? "true" : undefined, children: [_jsxs("div", { className: "flex min-w-0 items-start gap-2", children: [_jsx(CommentComposerAvatar, { author: author }), _jsx(InputGroupTextarea, { "aria-label": "Comment",
|
|
26
|
+
// An active composer opens a few lines of room; min-height beats
|
|
27
|
+
// the resize logic's inline height, so growth still works. The
|
|
28
|
+
// two min-heights are exclusive because cn resolves no conflicts,
|
|
29
|
+
// and important because the base Textarea's own min-h-16 is
|
|
30
|
+
// emitted after both and would otherwise win in every state.
|
|
31
|
+
className: cn("max-h-32 overflow-y-hidden px-0 py-1 text-sm leading-5", state.showActions ? "min-h-14!" : "min-h-7!"), disabled: isDisabled, onBlur: state.onBlur, onChange: state.onValueChange, onFocus: state.onFocus, onKeyDown: onComposerKeyDown, placeholder: placeholder, ref: textareaRef, rows: 1, value: state.value })] }), _jsx(CommentComposerActions, { actionsSlot: actionsSlot, canSubmit: state.canSubmit, isSubmitting: isSubmitting, loadingLabel: loadingLabel, showActions: state.showActions, submitLabel: submitLabel })] }) }));
|
|
23
32
|
}
|
|
24
33
|
function useCommentComposerState({ defaultExpanded, defaultValue, disabled, isSubmitting, onSubmit, onValueChange, value, }) {
|
|
25
34
|
const [localValue, setLocalValue] = React.useState(defaultValue);
|
|
@@ -108,6 +117,15 @@ function CommentComposerAvatar({ author }) {
|
|
|
108
117
|
function CommentSubmitButton({ canSubmit, isSubmitting, loadingLabel, submitLabel, }) {
|
|
109
118
|
return (_jsx(InputGroupButton, { disabled: !canSubmit, loading: isSubmitting, loadingLabel: loadingLabel, size: "sm", type: "submit", children: submitLabel }));
|
|
110
119
|
}
|
|
120
|
+
/** Cmd/Ctrl+Enter posts; plain Enter keeps writing, as in every comment box. */
|
|
121
|
+
function onComposerKeyDown(event) {
|
|
122
|
+
if (event.key !== "Enter" || !(event.metaKey || event.ctrlKey))
|
|
123
|
+
return;
|
|
124
|
+
if (event.nativeEvent.isComposing)
|
|
125
|
+
return; // mid-IME Enter confirms the composition
|
|
126
|
+
event.preventDefault();
|
|
127
|
+
event.currentTarget.form?.requestSubmit();
|
|
128
|
+
}
|
|
111
129
|
function resizeCommentTextarea(textarea) {
|
|
112
130
|
if (!textarea)
|
|
113
131
|
return;
|
|
@@ -1,11 +1,41 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import * as React from "react";
|
|
2
3
|
import { MessageSquareTextIcon } from "lucide-react";
|
|
3
4
|
import { CommentComposer } from "./comment-composer";
|
|
4
5
|
import { CommentItem } from "./comment-item";
|
|
5
6
|
import { Skeleton } from "../ui/skeleton";
|
|
6
7
|
function CommentThread({ comments, composerAuthor, composerPlaceholder, composerSubmitLabel, emptyDescription = "Start the conversation with a short update or question.", emptyTitle = "No comments yet", isLoading = false, isSubmitting = false, onSubmit, readOnly = false, renderCommentActions, showHeader = true, title = "Comments", }) {
|
|
7
|
-
const
|
|
8
|
-
|
|
8
|
+
const { armReveal, endRef } = useRevealThreadEnd(comments.length);
|
|
9
|
+
const submitAndReveal = !readOnly && onSubmit
|
|
10
|
+
? async (body) => {
|
|
11
|
+
await onSubmit(body);
|
|
12
|
+
armReveal();
|
|
13
|
+
}
|
|
14
|
+
: undefined;
|
|
15
|
+
return (_jsxs("section", { className: "relative grid w-full gap-(--panel-gap)", "aria-label": title, children: [showHeader ? _jsx(CommentThreadHeader, { count: comments.length, title: title }) : null, _jsx(CommentThreadBody, { comments: comments, emptyDescription: emptyDescription, emptyTitle: emptyTitle, isLoading: isLoading, renderCommentActions: renderCommentActions }), submitAndReveal ? (_jsx(CommentComposer, { author: composerAuthor, isSubmitting: isSubmitting, onSubmit: submitAndReveal, placeholder: composerPlaceholder, submitLabel: composerSubmitLabel })) : null, _jsx("div", { "aria-hidden": true, className: "absolute bottom-0", ref: endRef })] }));
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* After a send, the thread follows its own tail: scroll once when the submit
|
|
19
|
+
* resolves, and again when the posted comment actually lands in `comments` —
|
|
20
|
+
* the app may only append it after a refetch.
|
|
21
|
+
*/
|
|
22
|
+
function useRevealThreadEnd(commentCount) {
|
|
23
|
+
const endRef = React.useRef(null);
|
|
24
|
+
// A deadline, not a flag: the follow-up scroll only chases the send it
|
|
25
|
+
// belongs to, so a comment arriving minutes later cannot yank a reader
|
|
26
|
+
// who has scrolled away.
|
|
27
|
+
const armedUntil = React.useRef(0);
|
|
28
|
+
React.useEffect(() => {
|
|
29
|
+
if (armedUntil.current === 0 || Date.now() > armedUntil.current)
|
|
30
|
+
return;
|
|
31
|
+
armedUntil.current = 0;
|
|
32
|
+
endRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
33
|
+
}, [commentCount]);
|
|
34
|
+
const armReveal = React.useCallback(() => {
|
|
35
|
+
armedUntil.current = Date.now() + 5000;
|
|
36
|
+
endRef.current?.scrollIntoView({ behavior: "smooth", block: "nearest" });
|
|
37
|
+
}, []);
|
|
38
|
+
return { armReveal, endRef };
|
|
9
39
|
}
|
|
10
40
|
function CommentThreadHeader({ count, title }) {
|
|
11
41
|
return (_jsxs("header", { className: "flex items-center justify-between gap-3", children: [_jsx("h3", { className: "text-sm font-medium text-foreground", children: title }), _jsx("span", { className: "font-mono text-xs text-muted-foreground tabular-nums", children: count })] }));
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
import { type TimestampFormatOptions } from "./timestamp-format";
|
|
1
|
+
import { toUtcTimestamp, type TimestampFormatOptions } from "./timestamp-format";
|
|
2
2
|
declare function formatFeedbackTimestamp(value: string | undefined, options?: TimestampFormatOptions): string;
|
|
3
3
|
declare function formatFeedbackTimestampFull(value: string | undefined, options?: TimestampFormatOptions): string;
|
|
4
4
|
declare function getFeedbackTimestampDateTime(value: string | undefined): string | undefined;
|
|
5
|
-
export { formatFeedbackTimestamp, formatFeedbackTimestampFull, getFeedbackTimestampDateTime };
|
|
5
|
+
export { formatFeedbackTimestamp, formatFeedbackTimestampFull, getFeedbackTimestampDateTime, toUtcTimestamp, };
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { formatTimestamp, formatTimestampFull, getTimestampDateTime, } from "./timestamp-format";
|
|
1
|
+
import { formatTimestamp, formatTimestampFull, getTimestampDateTime, toUtcTimestamp, } from "./timestamp-format";
|
|
2
2
|
function formatFeedbackTimestamp(value, options = {}) {
|
|
3
3
|
return formatTimestamp(value, options);
|
|
4
4
|
}
|
|
@@ -8,4 +8,4 @@ function formatFeedbackTimestampFull(value, options = {}) {
|
|
|
8
8
|
function getFeedbackTimestampDateTime(value) {
|
|
9
9
|
return getTimestampDateTime(value);
|
|
10
10
|
}
|
|
11
|
-
export { formatFeedbackTimestamp, formatFeedbackTimestampFull, getFeedbackTimestampDateTime };
|
|
11
|
+
export { formatFeedbackTimestamp, formatFeedbackTimestampFull, getFeedbackTimestampDateTime, toUtcTimestamp, };
|
|
@@ -9,7 +9,7 @@ import { Avatar, AvatarFallback, AvatarImage } from "../ui/avatar";
|
|
|
9
9
|
import { Button } from "../ui/button";
|
|
10
10
|
import { Card } from "../ui/card";
|
|
11
11
|
import { Item, ItemActions, ItemContent, ItemDescription, ItemGroup, ItemGroupItem, ItemMedia, ItemTitle, } from "../ui/item";
|
|
12
|
-
import {
|
|
12
|
+
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "../ui/select";
|
|
13
13
|
import { PropertyList, PropertyListItem, PropertyListLabel, PropertyListValue, } from "../ui/property-list";
|
|
14
14
|
import { Sheet, SheetBody, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetTitle, } from "../ui/sheet";
|
|
15
15
|
function FeedbackDetailSheet({ conversation, feedback, onOpenChange, onStatusChange, open, pendingStatus, }) {
|
|
@@ -27,11 +27,11 @@ function FeedbackDetailContent({ feedback, onStatusChange, pendingStatus, }) {
|
|
|
27
27
|
*/
|
|
28
28
|
function FeedbackDetailStatusSection({ onStatusChange, pendingStatus, status, }) {
|
|
29
29
|
const selectId = React.useId();
|
|
30
|
-
return (_jsxs("div", { className: "grid gap-2", children: [_jsx("label", { className: "text-meta text-muted-foreground", htmlFor: selectId, children: "Status" }),
|
|
31
|
-
const next = feedbackStatusValues.find((
|
|
30
|
+
return (_jsxs("div", { className: "grid gap-2", children: [_jsx("label", { className: "text-meta text-muted-foreground", htmlFor: selectId, children: "Status" }), _jsxs(Select, { disabled: pendingStatus !== undefined, onValueChange: (value) => {
|
|
31
|
+
const next = feedbackStatusValues.find((candidate) => candidate === value);
|
|
32
32
|
if (next && next !== status)
|
|
33
33
|
onStatusChange(next);
|
|
34
|
-
}, value: pendingStatus ?? status, children: feedbackStatusValues.map((value) => (_jsx(
|
|
34
|
+
}, value: pendingStatus ?? status, children: [_jsx(SelectTrigger, { className: "w-full", id: selectId, children: _jsx(SelectValue, {}) }), _jsx(SelectContent, { children: feedbackStatusValues.map((value) => (_jsx(SelectItem, { value: value, children: feedbackStatusLabels[value] }, value))) })] })] }));
|
|
35
35
|
}
|
|
36
36
|
function FeedbackDetailSummary({ feedback }) {
|
|
37
37
|
const submitter = feedback.submittedBy;
|
|
@@ -49,6 +49,12 @@ type FeedbackMetrics = {
|
|
|
49
49
|
received: number;
|
|
50
50
|
total: number;
|
|
51
51
|
};
|
|
52
|
+
type FeedbackTimelineEvent = {
|
|
53
|
+
actorEmail: string;
|
|
54
|
+
actorRole: "reporter" | "support";
|
|
55
|
+
at: string;
|
|
56
|
+
toStatus: FeedbackStatus;
|
|
57
|
+
};
|
|
52
58
|
declare const feedbackStatusLabels: Record<FeedbackStatus, string>;
|
|
53
59
|
declare const feedbackTypeLabels: Record<StakeholderFeedbackType, string>;
|
|
54
60
|
declare const feedbackStatusOptions: readonly [{
|
|
@@ -84,4 +90,4 @@ declare const feedbackTypeOptions: readonly [{
|
|
|
84
90
|
readonly value: "general";
|
|
85
91
|
}];
|
|
86
92
|
declare function getFeedbackInitials(name: string, email: string | undefined): string;
|
|
87
|
-
export { feedbackStatusLabels, feedbackStatusOptions, feedbackStatusValues, feedbackTypeLabels, feedbackTypeOptions, getFeedbackInitials, type FeedbackAffectedScope, type FeedbackAttachment, type FeedbackMetrics, type FeedbackStatus, type FeedbackSubmittedBy, type FeedbackUrgency, type StakeholderFeedbackItem, type StakeholderFeedbackType, type UserFeedbackItem, };
|
|
93
|
+
export { feedbackStatusLabels, feedbackStatusOptions, feedbackStatusValues, feedbackTypeLabels, feedbackTypeOptions, getFeedbackInitials, type FeedbackAffectedScope, type FeedbackAttachment, type FeedbackMetrics, type FeedbackStatus, type FeedbackSubmittedBy, type FeedbackTimelineEvent, type FeedbackUrgency, type StakeholderFeedbackItem, type StakeholderFeedbackType, type UserFeedbackItem, };
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { type FeedbackTimelineEvent } from "./feedback-tracking-types";
|
|
2
|
+
type FeedbackTriageTimelineProps = {
|
|
3
|
+
events: FeedbackTimelineEvent[];
|
|
4
|
+
};
|
|
5
|
+
/**
|
|
6
|
+
* The item's triage history, oldest first: where it has been, who moved it,
|
|
7
|
+
* and when. Support identities read as "Support" -- one team voice, the same
|
|
8
|
+
* rule the conversation applies.
|
|
9
|
+
*/
|
|
10
|
+
declare function FeedbackTriageTimeline({ events }: FeedbackTriageTimelineProps): import("@types/react").JSX.Element | null;
|
|
11
|
+
export { FeedbackTriageTimeline, type FeedbackTriageTimelineProps };
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { formatFeedbackTimestamp } from "./feedback-date-format";
|
|
3
|
+
import { feedbackStatusLabels } from "./feedback-tracking-types";
|
|
4
|
+
/**
|
|
5
|
+
* The item's triage history, oldest first: where it has been, who moved it,
|
|
6
|
+
* and when. Support identities read as "Support" -- one team voice, the same
|
|
7
|
+
* rule the conversation applies.
|
|
8
|
+
*/
|
|
9
|
+
function FeedbackTriageTimeline({ events }) {
|
|
10
|
+
if (events.length === 0)
|
|
11
|
+
return null;
|
|
12
|
+
return (_jsxs("section", { "aria-label": "Triage history", className: "grid gap-1.5", children: [_jsx("h3", { className: "text-sm font-medium", children: "Timeline" }), _jsx("ol", { className: "grid gap-1 border-l pl-3", children: events.map((event, index) => (_jsxs("li", { className: "text-xs text-muted-foreground", children: [_jsx("span", { className: "font-medium text-foreground", children: feedbackStatusLabels[event.toStatus] }), " · ", event.actorRole === "support" ? "Support" : event.actorEmail, " · ", formatFeedbackTimestamp(event.at)] }, getTimelineEventKey(event, index)))) })] }));
|
|
13
|
+
}
|
|
14
|
+
function getTimelineEventKey(event, index) {
|
|
15
|
+
return `${event.toStatus}-${event.at}-${index}`; // two transitions can share a status and timestamp
|
|
16
|
+
}
|
|
17
|
+
export { FeedbackTriageTimeline };
|
|
@@ -1,6 +1,11 @@
|
|
|
1
|
+
import type * as React from "react";
|
|
1
2
|
import { type StakeholderFeedbackType } from "./feedback-tracking-types";
|
|
2
3
|
type FeedbackTypeBadgeProps = {
|
|
3
4
|
type: StakeholderFeedbackType;
|
|
4
5
|
};
|
|
5
|
-
|
|
6
|
+
/**
|
|
7
|
+
* Quiet on purpose: status owns hue wherever the two badges share a row, so
|
|
8
|
+
* the type reads by its icon's silhouette instead of competing for color.
|
|
9
|
+
*/
|
|
10
|
+
declare function FeedbackTypeBadge({ type }: FeedbackTypeBadgeProps): React.JSX.Element;
|
|
6
11
|
export { FeedbackTypeBadge, type FeedbackTypeBadgeProps };
|
|
@@ -1,12 +1,18 @@
|
|
|
1
|
-
import { jsx as _jsx } from "react/jsx-runtime";
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { BugIcon, LightbulbIcon, MessageSquareIcon } from "lucide-react";
|
|
2
3
|
import { Badge } from "../ui/badge";
|
|
3
4
|
import { feedbackTypeLabels } from "./feedback-tracking-types";
|
|
5
|
+
/**
|
|
6
|
+
* Quiet on purpose: status owns hue wherever the two badges share a row, so
|
|
7
|
+
* the type reads by its icon's silhouette instead of competing for color.
|
|
8
|
+
*/
|
|
4
9
|
function FeedbackTypeBadge({ type }) {
|
|
5
|
-
|
|
10
|
+
const Icon = feedbackTypeIcons[type];
|
|
11
|
+
return (_jsxs(Badge, { variant: "outline", children: [_jsx(Icon, {}), feedbackTypeLabels[type]] }));
|
|
6
12
|
}
|
|
7
|
-
const
|
|
8
|
-
bug:
|
|
9
|
-
feature:
|
|
10
|
-
general:
|
|
13
|
+
const feedbackTypeIcons = {
|
|
14
|
+
bug: BugIcon,
|
|
15
|
+
feature: LightbulbIcon,
|
|
16
|
+
general: MessageSquareIcon,
|
|
11
17
|
};
|
|
12
18
|
export { FeedbackTypeBadge };
|
|
@@ -0,0 +1,6 @@
|
|
|
1
|
+
type FeedbackUnreadBadgeProps = {
|
|
2
|
+
count: number;
|
|
3
|
+
};
|
|
4
|
+
/** "N new reply/replies" pill for list rows; renders nothing at zero. */
|
|
5
|
+
declare function FeedbackUnreadBadge({ count }: FeedbackUnreadBadgeProps): import("@types/react").JSX.Element | null;
|
|
6
|
+
export { FeedbackUnreadBadge, type FeedbackUnreadBadgeProps };
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
import { jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { Badge } from "../ui/badge";
|
|
3
|
+
/** "N new reply/replies" pill for list rows; renders nothing at zero. */
|
|
4
|
+
function FeedbackUnreadBadge({ count }) {
|
|
5
|
+
if (count <= 0)
|
|
6
|
+
return null;
|
|
7
|
+
return (_jsxs(Badge, { variant: "secondary", children: [count, " new ", count === 1 ? "reply" : "replies"] }));
|
|
8
|
+
}
|
|
9
|
+
export { FeedbackUnreadBadge };
|
|
@@ -18,7 +18,7 @@ function FeedbackUserDetailHeader({ feedback, onBack, }) {
|
|
|
18
18
|
return (_jsx("header", { className: "grid gap-3", children: _jsxs("div", { className: "flex min-w-0 items-center gap-2", children: [onBack ? _jsx(FeedbackUserDetailBackButton, { onBack: onBack }) : null, _jsx("div", { className: "min-w-0 flex-1", children: _jsxs("div", { className: "flex min-w-0 items-center justify-between gap-3", children: [_jsx("h3", { className: "truncate text-sm font-medium text-foreground", children: feedback.subject }), _jsx(FeedbackUserDetailBadges, { feedback: feedback })] }) })] }) }));
|
|
19
19
|
}
|
|
20
20
|
function FeedbackUserDetailBadges({ feedback }) {
|
|
21
|
-
return (_jsxs("div", { className: "flex shrink-0 flex-wrap justify-end gap-2", children: [feedback.hasUnreadReply ? _jsx(Badge, { variant: "secondary", children: "New
|
|
21
|
+
return (_jsxs("div", { className: "flex shrink-0 flex-wrap justify-end gap-2", children: [feedback.hasUnreadReply ? _jsx(Badge, { variant: "secondary", children: "New activity" }) : null, feedback.urgency === "emergency" ? (_jsxs(Badge, { variant: "destructive", children: [_jsx(AlertTriangleIcon, {}), "Emergency"] })) : null, _jsx(FeedbackTypeBadge, { type: feedback.type }), _jsx(FeedbackStatusBadge, { status: feedback.status })] }));
|
|
22
22
|
}
|
|
23
23
|
function FeedbackUserDetailBackButton({ onBack }) {
|
|
24
24
|
return (_jsx(Button, { "aria-label": "Back to my feedback", className: "-ml-2 shrink-0", onClick: onBack, size: "icon", type: "button", variant: "ghost", children: _jsx(ArrowLeftIcon, {}) }));
|
|
@@ -26,7 +26,7 @@ function FeedbackUserListItem({ item, onSelectFeedback, selected, }) {
|
|
|
26
26
|
return (_jsx(ItemGroupItem, { children: _jsx(Item, { "aria-current": selected ? "true" : undefined, asChild: true, className: cn("w-full cursor-pointer text-left", selected && "border-foreground/30 bg-muted/50"), size: "sm", variant: "outline", children: _jsx("button", { onClick: () => onSelectFeedback?.(item), type: "button", children: _jsx(FeedbackUserListItemContent, { item: item }) }) }) }));
|
|
27
27
|
}
|
|
28
28
|
function FeedbackUserListItemContent({ item }) {
|
|
29
|
-
return (_jsxs(_Fragment, { children: [_jsxs(ItemContent, { className: "min-w-0", children: [_jsx(ItemTitle, { className: "max-w-full truncate", children: item.subject }), _jsx(ItemDescription, { className: "line-clamp-1", children: item.description })] }), _jsxs(ItemActions, { className: "shrink-0", children: [item.hasUnreadReply ? _jsx(Badge, { variant: "secondary", children: "New
|
|
29
|
+
return (_jsxs(_Fragment, { children: [_jsxs(ItemContent, { className: "min-w-0", children: [_jsx(ItemTitle, { className: "max-w-full truncate", children: item.subject }), _jsx(ItemDescription, { className: "line-clamp-1", children: item.description })] }), _jsxs(ItemActions, { className: "shrink-0", children: [item.hasUnreadReply ? _jsx(Badge, { variant: "secondary", children: "New activity" }) : null, item.urgency === "emergency" ? (_jsxs(Badge, { variant: "destructive", children: [_jsx(AlertTriangleIcon, {}), "Emergency"] })) : null, _jsx(FeedbackTypeBadge, { type: item.type }), _jsx(FeedbackStatusBadge, { status: item.status })] }), _jsxs(ItemFooter, { children: [_jsx(FeedbackListDate, { label: "Submitted", value: item.submittedAt }), item.updatedAt ? _jsx(FeedbackListDate, { label: "Updated", value: item.updatedAt }) : null] })] }));
|
|
30
30
|
}
|
|
31
31
|
function FeedbackListDate({ label, value }) {
|
|
32
32
|
return (_jsxs("span", { className: "font-mono text-xs text-muted-foreground tabular-nums", children: [label, " ", formatFeedbackTimestamp(value)] }));
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { McpToolInventoryState } from "./mcp-access-tool-picker-internals";
|
|
2
|
+
import type { McpAccessToolSelector } from "./mcp-access-types";
|
|
3
|
+
/**
|
|
4
|
+
* The tool picker for inventories big enough to need navigation: one scroll
|
|
5
|
+
* region grouped by each tool's first OpenAPI tag, each group split into Read
|
|
6
|
+
* and Actions with a select-all per split, and a filter box on top. An
|
|
7
|
+
* inventory with no tags renders as a single unlabeled group, so the split,
|
|
8
|
+
* the select-alls, and the filter still apply.
|
|
9
|
+
*
|
|
10
|
+
* Every change is rebuilt from the full inventory against the one shared
|
|
11
|
+
* selection, so a tool selected but hidden by the filter is never dropped —
|
|
12
|
+
* only the rows on screen, and the select-alls over them, can change it.
|
|
13
|
+
*/
|
|
14
|
+
export declare function McpAccessGroupedToolPicker({ busy, inventory, selected, onChange, }: {
|
|
15
|
+
busy: boolean;
|
|
16
|
+
inventory: McpToolInventoryState;
|
|
17
|
+
selected: McpAccessToolSelector[];
|
|
18
|
+
onChange: (tools: McpAccessToolSelector[]) => void;
|
|
19
|
+
}): import("@types/react").JSX.Element;
|
|
@@ -0,0 +1,116 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs, Fragment as _Fragment } from "react/jsx-runtime";
|
|
2
|
+
import { useState } from "react";
|
|
3
|
+
import { Checkbox } from "../ui/checkbox";
|
|
4
|
+
import { Input } from "../ui/input";
|
|
5
|
+
import { Spinner } from "../ui/spinner";
|
|
6
|
+
import { EmptySelectionWarning, InventoryUnavailableNote, keyOf, PickerRow, } from "./mcp-access-tool-picker-internals";
|
|
7
|
+
const UNTAGGED_LABEL = "Other";
|
|
8
|
+
/**
|
|
9
|
+
* The tool picker for inventories big enough to need navigation: one scroll
|
|
10
|
+
* region grouped by each tool's first OpenAPI tag, each group split into Read
|
|
11
|
+
* and Actions with a select-all per split, and a filter box on top. An
|
|
12
|
+
* inventory with no tags renders as a single unlabeled group, so the split,
|
|
13
|
+
* the select-alls, and the filter still apply.
|
|
14
|
+
*
|
|
15
|
+
* Every change is rebuilt from the full inventory against the one shared
|
|
16
|
+
* selection, so a tool selected but hidden by the filter is never dropped —
|
|
17
|
+
* only the rows on screen, and the select-alls over them, can change it.
|
|
18
|
+
*/
|
|
19
|
+
export function McpAccessGroupedToolPicker({ busy, inventory, selected, onChange, }) {
|
|
20
|
+
const [query, setQuery] = useState("");
|
|
21
|
+
if (inventory.data === undefined) {
|
|
22
|
+
return _jsx(InventoryUnavailableNote, { isPending: inventory.isPending });
|
|
23
|
+
}
|
|
24
|
+
const tools = inventory.data.tools;
|
|
25
|
+
const selectedKeys = new Set(selected.map((tool) => keyOf(tool)));
|
|
26
|
+
const applyKeys = (keys, checked) => {
|
|
27
|
+
const next = new Set(selectedKeys);
|
|
28
|
+
for (const key of keys) {
|
|
29
|
+
if (checked)
|
|
30
|
+
next.add(key);
|
|
31
|
+
else
|
|
32
|
+
next.delete(key);
|
|
33
|
+
}
|
|
34
|
+
onChange(tools
|
|
35
|
+
.filter((tool) => next.has(keyOf(tool)))
|
|
36
|
+
.map((tool) => ({ method: tool.method, pathTemplate: tool.pathTemplate })));
|
|
37
|
+
};
|
|
38
|
+
const needle = query.trim().toLowerCase();
|
|
39
|
+
const groups = groupByFirstTag(tools);
|
|
40
|
+
const visibleGroups = groups
|
|
41
|
+
.map((group) => ({ ...group, visible: group.tools.filter((tool) => matches(tool, needle)) }))
|
|
42
|
+
.filter((group) => group.visible.length > 0);
|
|
43
|
+
const visibleCount = visibleGroups.reduce((count, group) => count + group.visible.length, 0);
|
|
44
|
+
const chosen = tools.filter((tool) => selectedKeys.has(keyOf(tool)));
|
|
45
|
+
return (_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)] gap-2", children: [_jsx(Input, { "aria-label": "Filter tools", className: "h-8 text-xs", onChange: (event) => {
|
|
46
|
+
setQuery(event.target.value);
|
|
47
|
+
}, placeholder: `Filter ${tools.length} tools…`, type: "search", value: query }), _jsx("div", { className: "max-h-96 overflow-y-auto rounded-md border border-border/60", children: visibleGroups.length === 0 ? (_jsx("p", { className: "text-muted-foreground px-3 py-2 text-xs", children: tools.length === 0 ? "This API exposes no tools." : "No tools match the filter." })) : (visibleGroups.map((group) => (_jsx(GroupSection, { busy: busy, group: group, onApply: applyKeys, selectedKeys: selectedKeys }, group.label ?? "")))) }), _jsx(PickerFooter, { busy: busy, chosen: chosen.length, matching: needle === "" ? null : visibleCount, total: tools.length })] }));
|
|
48
|
+
}
|
|
49
|
+
function PickerFooter({ busy, chosen, matching, total, }) {
|
|
50
|
+
return (_jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [busy ? _jsx(Spinner, { "aria-label": "Saving selection", className: "text-muted-foreground" }) : null, _jsxs("p", { "aria-live": "polite", className: "text-muted-foreground text-xs", children: [chosen, " of ", total, " tools selected"] }), matching === null ? null : (_jsxs("p", { className: "text-muted-foreground text-xs", children: [matching, " match", matching === 1 ? "es" : "", " the filter"] })), chosen === 0 ? _jsx(EmptySelectionWarning, {}) : null] }));
|
|
51
|
+
}
|
|
52
|
+
function matches(tool, needle) {
|
|
53
|
+
if (needle === "")
|
|
54
|
+
return true;
|
|
55
|
+
return [tool.name, tool.description, `${tool.method} ${tool.pathTemplate}`].some((text) => text.toLowerCase().includes(needle));
|
|
56
|
+
}
|
|
57
|
+
/** One tag group: its sticky header with counts, then the Read and Actions runs. */
|
|
58
|
+
function GroupSection({ busy, group, selectedKeys, onApply, }) {
|
|
59
|
+
const chosenInGroup = group.tools.filter((tool) => selectedKeys.has(keyOf(tool))).length;
|
|
60
|
+
return (_jsxs("section", { children: [group.label === null ? null : (_jsxs("h4", { className: "bg-background sticky top-0 z-10 flex items-baseline justify-between gap-2 border-b border-border/60 px-3 py-1.5 font-mono text-xs font-semibold", children: [_jsx("span", { className: "truncate", children: group.label }), _jsxs("span", { className: "text-muted-foreground shrink-0 text-[10px] font-normal tabular-nums", children: [chosenInGroup, " / ", group.tools.length] })] })), [
|
|
61
|
+
{ label: "Read", rows: group.visible.filter((tool) => tool.readOnly) },
|
|
62
|
+
{ label: "Actions", rows: group.visible.filter((tool) => !tool.readOnly) },
|
|
63
|
+
].map(({ label, rows }) => rows.length === 0 ? null : (_jsx(SubsectionRows, { busy: busy, groupLabel: group.label, label: label, onApply: onApply, rows: rows, selectedKeys: selectedKeys }, label)))] }));
|
|
64
|
+
}
|
|
65
|
+
/** One Read or Actions run within a group: its header with select-all, then rows. */
|
|
66
|
+
function SubsectionRows({ busy, groupLabel, label, rows, selectedKeys, onApply, }) {
|
|
67
|
+
const rowKeys = rows.map((tool) => keyOf(tool));
|
|
68
|
+
const picked = rowKeys.filter((key) => selectedKeys.has(key)).length;
|
|
69
|
+
const allState = picked === 0 ? false : picked === rowKeys.length ? true : "indeterminate";
|
|
70
|
+
return (_jsxs(_Fragment, { children: [_jsxs("div", { className: "flex items-center justify-between gap-2 px-3 pt-2 pb-1", children: [_jsx("span", { className: label === "Actions"
|
|
71
|
+
? "text-primary text-[10px] font-semibold tracking-wide uppercase"
|
|
72
|
+
: "text-muted-foreground text-[10px] font-semibold tracking-wide uppercase", children: label }), _jsx(Checkbox, { "aria-label": `Select all ${label.toLowerCase()}${groupLabel === null ? "" : ` in ${groupLabel}`}`, checked: allState, disabled: busy, onCheckedChange: (next) => {
|
|
73
|
+
// The rows on screen only: with a filter active this is
|
|
74
|
+
// "select what I searched for", never the hidden rest.
|
|
75
|
+
onApply(rowKeys, next === true);
|
|
76
|
+
} })] }), _jsx("ul", { className: "divide-y divide-border/50", children: rows.map((tool) => (_jsx(PickerRow, { checked: selectedKeys.has(keyOf(tool)), disabled: busy,
|
|
77
|
+
// The subheader above already says Read or Actions.
|
|
78
|
+
kindBadge: false, onToggle: (key, checked) => {
|
|
79
|
+
onApply([key], checked);
|
|
80
|
+
}, tool: tool }, keyOf(tool)))) })] }));
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Buckets by first tag in first-appearance order — a compiler emits tools in
|
|
84
|
+
* source order, so tag order is the API's own organization. Untagged tools
|
|
85
|
+
* trail as one "Other" bucket, unless nothing is tagged at all: then the one
|
|
86
|
+
* group is unlabeled rather than titled "Other" for no contrast.
|
|
87
|
+
*/
|
|
88
|
+
function groupByFirstTag(tools) {
|
|
89
|
+
const buckets = new Map();
|
|
90
|
+
for (const tool of tools) {
|
|
91
|
+
const firstTag = tool.tags?.[0];
|
|
92
|
+
if (firstTag === undefined)
|
|
93
|
+
continue;
|
|
94
|
+
const bucket = buckets.get(firstTag);
|
|
95
|
+
if (bucket)
|
|
96
|
+
bucket.push(tool);
|
|
97
|
+
else
|
|
98
|
+
buckets.set(firstTag, [tool]);
|
|
99
|
+
}
|
|
100
|
+
if (buckets.size === 0)
|
|
101
|
+
return [{ label: null, tools }];
|
|
102
|
+
const untagged = tools.filter((tool) => (tool.tags?.length ?? 0) === 0);
|
|
103
|
+
if (untagged.length > 0) {
|
|
104
|
+
// Into the real "Other" tag's bucket when one exists, so two groups
|
|
105
|
+
// never share a label (and with it a React key).
|
|
106
|
+
const bucket = buckets.get(UNTAGGED_LABEL);
|
|
107
|
+
if (bucket)
|
|
108
|
+
bucket.push(...untagged);
|
|
109
|
+
else
|
|
110
|
+
buckets.set(UNTAGGED_LABEL, untagged);
|
|
111
|
+
}
|
|
112
|
+
return [...buckets.entries()].map(([label, bucketTools]) => ({
|
|
113
|
+
label,
|
|
114
|
+
tools: bucketTools,
|
|
115
|
+
}));
|
|
116
|
+
}
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
import type { McpAccessToolListing } from "./mcp-access-types";
|
|
2
|
+
/** The coordinate a selector and a compiled operation are matched on. */
|
|
3
|
+
export declare const keyOf: (tool: {
|
|
4
|
+
method: string;
|
|
5
|
+
pathTemplate: string;
|
|
6
|
+
}) => string;
|
|
7
|
+
/**
|
|
8
|
+
* The slice of the app's inventory query the pickers read, shaped so a
|
|
9
|
+
* TanStack Query result can be passed whole. `data` is `undefined` for both
|
|
10
|
+
* "still loading" and "failed to load"; `isPending` says which.
|
|
11
|
+
*/
|
|
12
|
+
export type McpToolInventoryState = {
|
|
13
|
+
data: {
|
|
14
|
+
tools: McpAccessToolListing[];
|
|
15
|
+
} | undefined;
|
|
16
|
+
isPending: boolean;
|
|
17
|
+
};
|
|
18
|
+
/** The one line an absent inventory renders, in both pickers. */
|
|
19
|
+
export declare function InventoryUnavailableNote({ isPending }: {
|
|
20
|
+
isPending: boolean;
|
|
21
|
+
}): import("@types/react").JSX.Element;
|
|
22
|
+
/** Why an empty selection blocks the mint, in both pickers' footers. */
|
|
23
|
+
export declare function EmptySelectionWarning(): import("@types/react").JSX.Element;
|
|
24
|
+
/**
|
|
25
|
+
* One selectable tool row. `kindBadge` shows the read-only/action label — the
|
|
26
|
+
* flat list's only signal, redundant under the grouped picker's Read/Actions
|
|
27
|
+
* subheaders.
|
|
28
|
+
*/
|
|
29
|
+
export declare function PickerRow({ checked, disabled, kindBadge, tool, onToggle, }: {
|
|
30
|
+
checked: boolean;
|
|
31
|
+
disabled: boolean;
|
|
32
|
+
kindBadge: boolean;
|
|
33
|
+
tool: McpAccessToolListing;
|
|
34
|
+
onToggle: (key: string, checked: boolean) => void;
|
|
35
|
+
}): import("@types/react").JSX.Element;
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { useId } from "react";
|
|
3
|
+
import { Checkbox } from "../ui/checkbox";
|
|
4
|
+
/** The coordinate a selector and a compiled operation are matched on. */
|
|
5
|
+
export const keyOf = (tool) => `${tool.method} ${tool.pathTemplate}`;
|
|
6
|
+
/** The one line an absent inventory renders, in both pickers. */
|
|
7
|
+
export function InventoryUnavailableNote({ isPending }) {
|
|
8
|
+
return (_jsx("p", { className: "text-muted-foreground text-xs", children: isPending ? "Loading the tool list…" : "Couldn't load the tool list." }));
|
|
9
|
+
}
|
|
10
|
+
/** Why an empty selection blocks the mint, in both pickers' footers. */
|
|
11
|
+
export function EmptySelectionWarning() {
|
|
12
|
+
return (_jsx("p", { className: "text-destructive text-xs", children: "Select at least one tool to generate a connection code." }));
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* One selectable tool row. `kindBadge` shows the read-only/action label — the
|
|
16
|
+
* flat list's only signal, redundant under the grouped picker's Read/Actions
|
|
17
|
+
* subheaders.
|
|
18
|
+
*/
|
|
19
|
+
export function PickerRow({ checked, disabled, kindBadge, tool, onToggle, }) {
|
|
20
|
+
const id = useId();
|
|
21
|
+
return (_jsxs("li", { className: "flex min-h-11 items-start gap-3 px-3 py-2", children: [_jsx(Checkbox, { checked: checked, disabled: disabled, id: id, onCheckedChange: (next) => {
|
|
22
|
+
onToggle(keyOf(tool), next === true);
|
|
23
|
+
} }), _jsxs("label", { className: "min-w-0 flex-1 cursor-pointer", htmlFor: id, children: [kindBadge ? (_jsxs("span", { className: "flex flex-wrap items-center gap-x-2", children: [_jsx("span", { className: "truncate font-mono text-xs", children: tool.name }), _jsx("span", { className: "text-muted-foreground text-[10px] uppercase", children: tool.readOnly ? "read-only" : "action" })] })) : (_jsx("span", { className: "block truncate font-mono text-xs", children: tool.name })), _jsx("span", { className: "text-muted-foreground block truncate text-xs", children: tool.description })] })] }));
|
|
24
|
+
}
|
|
@@ -1,20 +1,7 @@
|
|
|
1
|
-
import type {
|
|
2
|
-
|
|
3
|
-
export
|
|
4
|
-
|
|
5
|
-
pathTemplate: string;
|
|
6
|
-
}) => string;
|
|
7
|
-
/**
|
|
8
|
-
* The slice of the app's inventory query the picker reads, shaped so a
|
|
9
|
-
* TanStack Query result can be passed whole. `data` is `undefined` for both
|
|
10
|
-
* "still loading" and "failed to load"; `isPending` says which.
|
|
11
|
-
*/
|
|
12
|
-
export type McpToolInventoryState = {
|
|
13
|
-
data: {
|
|
14
|
-
tools: McpAccessToolListing[];
|
|
15
|
-
} | undefined;
|
|
16
|
-
isPending: boolean;
|
|
17
|
-
};
|
|
1
|
+
import type { McpToolInventoryState } from "./mcp-access-tool-picker-internals";
|
|
2
|
+
import type { McpAccessToolSelector } from "./mcp-access-types";
|
|
3
|
+
export { keyOf } from "./mcp-access-tool-picker-internals";
|
|
4
|
+
export type { McpToolInventoryState } from "./mcp-access-tool-picker-internals";
|
|
18
5
|
/**
|
|
19
6
|
* An exact tool selection, applied as it is clicked. There is nothing to
|
|
20
7
|
* confirm: the list on screen is always what a code will be minted with, and an
|
|
@@ -1,9 +1,7 @@
|
|
|
1
1
|
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
-
import { useId } from "react";
|
|
3
|
-
import { Checkbox } from "../ui/checkbox";
|
|
4
2
|
import { Spinner } from "../ui/spinner";
|
|
5
|
-
|
|
6
|
-
export
|
|
3
|
+
import { EmptySelectionWarning, InventoryUnavailableNote, keyOf, PickerRow, } from "./mcp-access-tool-picker-internals";
|
|
4
|
+
export { keyOf } from "./mcp-access-tool-picker-internals";
|
|
7
5
|
/**
|
|
8
6
|
* An exact tool selection, applied as it is clicked. There is nothing to
|
|
9
7
|
* confirm: the list on screen is always what a code will be minted with, and an
|
|
@@ -17,7 +15,7 @@ export const keyOf = (tool) => `${tool.method} ${tool.pathTemplate}`;
|
|
|
17
15
|
export function McpAccessToolPicker({ busy, inventory, selected, onChange, }) {
|
|
18
16
|
const selectedKeys = new Set(selected.map((tool) => keyOf(tool)));
|
|
19
17
|
if (inventory.data === undefined) {
|
|
20
|
-
return
|
|
18
|
+
return _jsx(InventoryUnavailableNote, { isPending: inventory.isPending });
|
|
21
19
|
}
|
|
22
20
|
const tools = inventory.data.tools;
|
|
23
21
|
const toggle = (key, checked) => {
|
|
@@ -31,11 +29,5 @@ export function McpAccessToolPicker({ busy, inventory, selected, onChange, }) {
|
|
|
31
29
|
.map((tool) => ({ method: tool.method, pathTemplate: tool.pathTemplate })));
|
|
32
30
|
};
|
|
33
31
|
const chosen = tools.filter((tool) => selectedKeys.has(keyOf(tool)));
|
|
34
|
-
return (_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)] gap-2", children: [_jsx("ul", { className: "max-h-64 divide-y divide-border/50 overflow-y-auto rounded-md border border-border/60", children: tools.map((tool) => (_jsx(PickerRow, { checked: selectedKeys.has(keyOf(tool)), disabled: busy, onToggle: toggle, tool: tool }, keyOf(tool)))) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [busy ? _jsx(Spinner, { "aria-label": "Saving selection", className: "text-muted-foreground" }) : null, _jsxs("p", { "aria-live": "polite", className: "text-muted-foreground text-xs", children: [chosen.length, " of ", tools.length, " tools selected"] }), chosen.length === 0 ?
|
|
35
|
-
}
|
|
36
|
-
function PickerRow({ checked, disabled, tool, onToggle, }) {
|
|
37
|
-
const id = useId();
|
|
38
|
-
return (_jsxs("li", { className: "flex min-h-11 items-start gap-3 px-3 py-2", children: [_jsx(Checkbox, { checked: checked, disabled: disabled, id: id, onCheckedChange: (next) => {
|
|
39
|
-
onToggle(keyOf(tool), next === true);
|
|
40
|
-
} }), _jsxs("label", { className: "min-w-0 flex-1 cursor-pointer", htmlFor: id, children: [_jsxs("span", { className: "flex flex-wrap items-center gap-x-2", children: [_jsx("span", { className: "truncate font-mono text-xs", children: tool.name }), _jsx("span", { className: "text-muted-foreground text-[10px] uppercase", children: tool.readOnly ? "read-only" : "action" })] }), _jsx("span", { className: "block truncate text-muted-foreground text-xs", children: tool.description })] })] }));
|
|
32
|
+
return (_jsxs("div", { className: "grid grid-cols-[minmax(0,1fr)] gap-2", children: [_jsx("ul", { className: "max-h-64 divide-y divide-border/50 overflow-y-auto rounded-md border border-border/60", children: tools.map((tool) => (_jsx(PickerRow, { checked: selectedKeys.has(keyOf(tool)), disabled: busy, kindBadge: true, onToggle: toggle, tool: tool }, keyOf(tool)))) }), _jsxs("div", { className: "flex flex-wrap items-center gap-2", children: [busy ? _jsx(Spinner, { "aria-label": "Saving selection", className: "text-muted-foreground" }) : null, _jsxs("p", { "aria-live": "polite", className: "text-muted-foreground text-xs", children: [chosen.length, " of ", tools.length, " tools selected"] }), chosen.length === 0 ? _jsx(EmptySelectionWarning, {}) : null] })] }));
|
|
41
33
|
}
|
|
@@ -21,6 +21,8 @@ export type McpAccessToolListing = {
|
|
|
21
21
|
readOnly: boolean;
|
|
22
22
|
method: McpHttpMethod;
|
|
23
23
|
pathTemplate: string;
|
|
24
|
+
/** The operation's OpenAPI tags; the grouped picker buckets by the first. */
|
|
25
|
+
tags?: readonly string[];
|
|
24
26
|
};
|
|
25
27
|
/**
|
|
26
28
|
* What a mint produced, as the fields these controls report: the access the
|
|
@@ -4,5 +4,11 @@ type TimestampFormatOptions = {
|
|
|
4
4
|
declare function formatTimestamp(value: string | undefined, options?: TimestampFormatOptions): string;
|
|
5
5
|
declare function formatTimestampFull(value: string | undefined, options?: TimestampFormatOptions): string;
|
|
6
6
|
declare function getTimestampDateTime(value: string | undefined): string | undefined;
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Backends that store naive UTC timestamps serialize without a zone designator,
|
|
9
|
+
* and `Date.parse` reads a zoneless string as local time. Append the missing
|
|
10
|
+
* designator so timestamps render in the viewer's timezone.
|
|
11
|
+
*/
|
|
12
|
+
declare function toUtcTimestamp(value: string): string;
|
|
13
|
+
export { formatTimestamp, formatTimestampFull, getTimestampDateTime, toUtcTimestamp };
|
|
8
14
|
export type { TimestampFormatOptions };
|
|
@@ -28,6 +28,14 @@ function getTimestampDateTime(value) {
|
|
|
28
28
|
return undefined;
|
|
29
29
|
return date.toISOString();
|
|
30
30
|
}
|
|
31
|
+
/**
|
|
32
|
+
* Backends that store naive UTC timestamps serialize without a zone designator,
|
|
33
|
+
* and `Date.parse` reads a zoneless string as local time. Append the missing
|
|
34
|
+
* designator so timestamps render in the viewer's timezone.
|
|
35
|
+
*/
|
|
36
|
+
function toUtcTimestamp(value) {
|
|
37
|
+
return /(?:z|[+-]\d\d:?\d\d)$/i.test(value) ? value : `${value}Z`;
|
|
38
|
+
}
|
|
31
39
|
function parseTimestamp(value) {
|
|
32
40
|
if (!value)
|
|
33
41
|
return undefined;
|
|
@@ -63,4 +71,4 @@ const dateTimeFormatter = new Intl.DateTimeFormat(undefined, {
|
|
|
63
71
|
month: "short",
|
|
64
72
|
year: "numeric",
|
|
65
73
|
});
|
|
66
|
-
export { formatTimestamp, formatTimestampFull, getTimestampDateTime };
|
|
74
|
+
export { formatTimestamp, formatTimestampFull, getTimestampDateTime, toUtcTimestamp };
|
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
"use client";
|
|
2
2
|
import { jsx as _jsx } from "react/jsx-runtime";
|
|
3
|
-
import { CheckIcon } from "lucide-react";
|
|
3
|
+
import { CheckIcon, MinusIcon } from "lucide-react";
|
|
4
4
|
import { Checkbox as CheckboxPrimitive } from "radix-ui";
|
|
5
5
|
import { cn } from "../../lib/utils";
|
|
6
6
|
function Checkbox({ className, ...props }) {
|
|
7
|
-
return (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", className: cn("peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[var(--focus-ring-width)] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary", className), ...props, children: _jsx(CheckboxPrimitive.Indicator, { "data-slot": "checkbox-indicator", className: "grid place-content-center text-current transition-none", children: _jsx(CheckIcon, { className: "size-3.5" }) }) }));
|
|
7
|
+
return (_jsx(CheckboxPrimitive.Root, { "data-slot": "checkbox", className: cn("peer size-4 shrink-0 rounded-[4px] border border-input shadow-xs transition-shadow outline-none focus-visible:border-ring focus-visible:ring-[var(--focus-ring-width)] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=checked]:border-primary data-[state=checked]:bg-primary data-[state=checked]:text-primary-foreground data-[state=indeterminate]:border-primary data-[state=indeterminate]:bg-primary data-[state=indeterminate]:text-primary-foreground dark:bg-input/30 dark:aria-invalid:ring-destructive/40 dark:data-[state=checked]:bg-primary dark:data-[state=indeterminate]:bg-primary", className), ...props, children: _jsx(CheckboxPrimitive.Indicator, { "data-slot": "checkbox-indicator", className: "grid place-content-center text-current transition-none", children: props.checked === "indeterminate" ? (_jsx(MinusIcon, { className: "size-3.5" })) : (_jsx(CheckIcon, { className: "size-3.5" })) }) }));
|
|
8
8
|
}
|
|
9
9
|
export { Checkbox };
|
|
@@ -7,17 +7,33 @@ type DataTableFacetOption = {
|
|
|
7
7
|
value: string;
|
|
8
8
|
};
|
|
9
9
|
/**
|
|
10
|
-
*
|
|
10
|
+
* The facet-filter popover, controlled: one button per facet that wears the
|
|
11
|
+
* current selection, checkable values inside. The caller owns the selection --
|
|
12
|
+
* URL search state, column filters, anything -- and receives the whole next
|
|
13
|
+
* selection on every toggle, so the popover can never disagree with the list
|
|
14
|
+
* it narrows and there is nothing to apply or discard on close.
|
|
15
|
+
*/
|
|
16
|
+
declare function FacetFilter({ counts, onSelectionChange, options, selected, title, }: {
|
|
17
|
+
/** Rows each option would leave, shown when provided. */
|
|
18
|
+
counts?: ReadonlyMap<string, number>;
|
|
19
|
+
onSelectionChange: (values: string[]) => void;
|
|
20
|
+
options: DataTableFacetOption[];
|
|
21
|
+
selected: readonly string[];
|
|
22
|
+
title: string;
|
|
23
|
+
}): React.JSX.Element;
|
|
24
|
+
/**
|
|
25
|
+
* The column-bound binding: narrow a column to a set of its own values.
|
|
11
26
|
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
27
|
+
* Selection lives in the column's filter value, and counts come from the
|
|
28
|
+
* table's faceted row model, which `TanStackDataTable` already builds -- so an
|
|
29
|
+
* option can say how many rows it would leave without the caller counting
|
|
30
|
+
* anything itself. Pair with the `arrIncludesSome` filter function, which is
|
|
31
|
+
* what a multi-select selection means.
|
|
16
32
|
*/
|
|
17
33
|
declare function DataTableFacetFilter({ columnId, options, title, }: {
|
|
18
34
|
columnId: string;
|
|
19
35
|
options: DataTableFacetOption[];
|
|
20
36
|
title: string;
|
|
21
37
|
}): React.JSX.Element;
|
|
22
|
-
export { DataTableFacetFilter };
|
|
38
|
+
export { DataTableFacetFilter, FacetFilter };
|
|
23
39
|
export type { DataTableFacetOption };
|
|
@@ -8,32 +8,44 @@ import { Command, CommandEmpty, CommandGroup, CommandInput, CommandItem, Command
|
|
|
8
8
|
import { Popover, PopoverContent, PopoverTrigger } from "./popover";
|
|
9
9
|
import { Separator } from "./separator";
|
|
10
10
|
/**
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
*
|
|
16
|
-
* function, which is what a multi-select selection means.
|
|
11
|
+
* The facet-filter popover, controlled: one button per facet that wears the
|
|
12
|
+
* current selection, checkable values inside. The caller owns the selection --
|
|
13
|
+
* URL search state, column filters, anything -- and receives the whole next
|
|
14
|
+
* selection on every toggle, so the popover can never disagree with the list
|
|
15
|
+
* it narrows and there is nothing to apply or discard on close.
|
|
17
16
|
*/
|
|
18
|
-
function
|
|
19
|
-
const
|
|
20
|
-
const
|
|
21
|
-
|
|
22
|
-
const select = (value) => {
|
|
23
|
-
const next = new Set(selected);
|
|
17
|
+
function FacetFilter({ counts, onSelectionChange, options, selected, title, }) {
|
|
18
|
+
const selectedSet = new Set(selected);
|
|
19
|
+
const toggle = (value) => {
|
|
20
|
+
const next = new Set(selectedSet);
|
|
24
21
|
if (next.has(value))
|
|
25
22
|
next.delete(value);
|
|
26
23
|
else
|
|
27
24
|
next.add(value);
|
|
28
|
-
|
|
29
|
-
column?.setFilterValue(next.size ? [...next] : undefined);
|
|
25
|
+
onSelectionChange([...next]);
|
|
30
26
|
};
|
|
31
|
-
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { size: "sm", type: "button", variant: "outline", children: [_jsx(PlusCircleIcon, {}), title,
|
|
32
|
-
|
|
33
|
-
}, option: option, selected:
|
|
34
|
-
|
|
27
|
+
return (_jsxs(Popover, { children: [_jsx(PopoverTrigger, { asChild: true, children: _jsxs(Button, { size: "sm", type: "button", variant: "outline", children: [_jsx(PlusCircleIcon, {}), title, selectedSet.size > 0 ? (_jsxs(_Fragment, { children: [_jsx(Separator, { className: "mx-0.5 data-[orientation=vertical]:h-4", orientation: "vertical" }), _jsx(FacetSelectionSummary, { labels: options.filter((option) => selectedSet.has(option.value)) })] })) : null] }) }), _jsx(PopoverContent, { align: "start", className: "w-52 p-0", children: _jsxs(Command, { children: [_jsx(CommandInput, { placeholder: title }), _jsxs(CommandList, { children: [_jsx(CommandEmpty, { children: "No matches." }), _jsx(CommandGroup, { children: options.map((option) => (_jsx(FacetOptionItem, { count: counts?.get(option.value), onSelect: () => {
|
|
28
|
+
toggle(option.value);
|
|
29
|
+
}, option: option, selected: selectedSet.has(option.value) }, option.value))) }), selectedSet.size > 0 ? (_jsxs(_Fragment, { children: [_jsx(CommandSeparator, {}), _jsx(CommandGroup, { children: _jsx(CommandItem, { className: "justify-center", onSelect: () => {
|
|
30
|
+
onSelectionChange([]);
|
|
35
31
|
}, children: "Clear" }) })] })) : null] })] }) })] }));
|
|
36
32
|
}
|
|
33
|
+
/**
|
|
34
|
+
* The column-bound binding: narrow a column to a set of its own values.
|
|
35
|
+
*
|
|
36
|
+
* Selection lives in the column's filter value, and counts come from the
|
|
37
|
+
* table's faceted row model, which `TanStackDataTable` already builds -- so an
|
|
38
|
+
* option can say how many rows it would leave without the caller counting
|
|
39
|
+
* anything itself. Pair with the `arrIncludesSome` filter function, which is
|
|
40
|
+
* what a multi-select selection means.
|
|
41
|
+
*/
|
|
42
|
+
function DataTableFacetFilter({ columnId, options, title, }) {
|
|
43
|
+
const column = useDataTable().getColumn(columnId);
|
|
44
|
+
return (_jsx(FacetFilter, { counts: column?.getFacetedUniqueValues(), onSelectionChange: (values) => {
|
|
45
|
+
// An empty selection is no filter at all, not a filter matching nothing.
|
|
46
|
+
column?.setFilterValue(values.length ? values : undefined);
|
|
47
|
+
}, options: options, selected: readSelection(column?.getFilterValue()), title: title }));
|
|
48
|
+
}
|
|
37
49
|
/** Names the choices while they still fit, then counts them. */
|
|
38
50
|
function FacetSelectionSummary({ labels }) {
|
|
39
51
|
if (labels.length > 2)
|
|
@@ -50,4 +62,4 @@ function readSelection(value) {
|
|
|
50
62
|
return [];
|
|
51
63
|
return value.filter((entry) => typeof entry === "string");
|
|
52
64
|
}
|
|
53
|
-
export { DataTableFacetFilter };
|
|
65
|
+
export { DataTableFacetFilter, FacetFilter };
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
type DataTableSearchInputProps = {
|
|
2
|
+
ariaLabel: string;
|
|
3
|
+
onChange: (value: string) => void;
|
|
4
|
+
placeholder: string;
|
|
5
|
+
value: string;
|
|
6
|
+
};
|
|
7
|
+
/**
|
|
8
|
+
* Shows what is being typed. Any debounce that turns the value into an address
|
|
9
|
+
* belongs in the caller's table state, next to everything else that decides
|
|
10
|
+
* what the table is looking at -- so a Clear that cancels a search cancels the
|
|
11
|
+
* write still in flight along with it.
|
|
12
|
+
*/
|
|
13
|
+
declare function DataTableSearchInput({ ariaLabel, onChange, placeholder, value, }: DataTableSearchInputProps): import("@types/react").JSX.Element;
|
|
14
|
+
export { DataTableSearchInput, type DataTableSearchInputProps };
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import { jsx as _jsx, jsxs as _jsxs } from "react/jsx-runtime";
|
|
2
|
+
import { SearchIcon } from "lucide-react";
|
|
3
|
+
import { Input } from "./input";
|
|
4
|
+
/**
|
|
5
|
+
* Shows what is being typed. Any debounce that turns the value into an address
|
|
6
|
+
* belongs in the caller's table state, next to everything else that decides
|
|
7
|
+
* what the table is looking at -- so a Clear that cancels a search cancels the
|
|
8
|
+
* write still in flight along with it.
|
|
9
|
+
*/
|
|
10
|
+
function DataTableSearchInput({ ariaLabel, onChange, placeholder, value, }) {
|
|
11
|
+
return (_jsxs("div", { className: "relative w-full sm:max-w-64", children: [_jsx(SearchIcon, { className: "pointer-events-none absolute top-1/2 left-2.5 size-4 -translate-y-1/2 text-muted-foreground" }), _jsx(Input, { "aria-label": ariaLabel, className: "pl-8", onChange: (event) => {
|
|
12
|
+
onChange(event.target.value);
|
|
13
|
+
}, placeholder: placeholder, type: "search", value: value })] }));
|
|
14
|
+
}
|
|
15
|
+
export { DataTableSearchInput };
|
|
@@ -12,12 +12,16 @@ function ItemGroupItem({ role = "listitem", ...props }) {
|
|
|
12
12
|
function ItemSeparator({ className, ...props }) {
|
|
13
13
|
return (_jsx(Separator, { "data-slot": "item-separator", orientation: "horizontal", className: cn("my-0", className), ...props }));
|
|
14
14
|
}
|
|
15
|
-
const itemVariants = cva(
|
|
15
|
+
const itemVariants = cva(
|
|
16
|
+
// Each variant owns the border color outright: `cn` does not resolve
|
|
17
|
+
// Tailwind conflicts, so base + variant both setting it would leave the
|
|
18
|
+
// winner to the consuming app's CSS emission order.
|
|
19
|
+
"group/item flex flex-wrap items-center rounded-md border text-sm transition-colors duration-100 outline-none focus-visible:border-ring focus-visible:ring-[var(--focus-ring-width)] focus-visible:ring-ring/50 [a]:transition-colors [a]:hover:bg-accent/50", {
|
|
16
20
|
variants: {
|
|
17
21
|
variant: {
|
|
18
|
-
default: "bg-transparent",
|
|
22
|
+
default: "border-transparent bg-transparent",
|
|
19
23
|
outline: "border-border",
|
|
20
|
-
muted: "bg-muted/50",
|
|
24
|
+
muted: "border-transparent bg-muted/50",
|
|
21
25
|
},
|
|
22
26
|
size: {
|
|
23
27
|
default: "gap-(--panel-gap) p-(--panel-padding)",
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/ui/data-table-search-input";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/ui/data-table-search-input";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/feedback-triage-timeline";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/feedback-triage-timeline";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/feedback-unread-badge";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/feedback-unread-badge";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/mcp-access-grouped-tool-picker";
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export * from "../components/blocks/mcp-access-grouped-tool-picker";
|