banhaten 0.4.0 → 0.6.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/CHANGELOG.md +78 -1
- package/CONTRIBUTING.md +6 -4
- package/README.md +4 -2
- package/docs/design-spacing.md +13 -0
- package/docs/design-system/README.md +2 -0
- package/docs/design-system/appearances/default.md +1 -1
- package/docs/design-system/appearances/rounded.md +1 -1
- package/docs/design-system/appearances/sharp.md +1 -1
- package/docs/design-system/component-showcase-consistency-report.md +5 -5
- package/docs/design-system/components/README.md +15 -0
- package/docs/design-system/components/accordion.md +1 -1
- package/docs/design-system/components/activity-feed.md +1 -1
- package/docs/design-system/components/activity-heatmap.md +1 -1
- package/docs/design-system/components/agent-input.md +85 -0
- package/docs/design-system/components/alert-dialog.md +1 -1
- package/docs/design-system/components/alert.md +10 -4
- package/docs/design-system/components/app-header.md +97 -0
- package/docs/design-system/components/approval-card.md +134 -0
- package/docs/design-system/components/attribute.md +14 -2
- package/docs/design-system/components/avatar.md +1 -1
- package/docs/design-system/components/badge.md +1 -1
- package/docs/design-system/components/banner.md +1 -1
- package/docs/design-system/components/breadcrumbs.md +8 -3
- package/docs/design-system/components/button-group.md +1 -1
- package/docs/design-system/components/button.md +8 -5
- package/docs/design-system/components/card.md +15 -1
- package/docs/design-system/components/catalog-components.md +1 -1
- package/docs/design-system/components/chart.md +95 -24
- package/docs/design-system/components/checkbox.md +1 -1
- package/docs/design-system/components/collapsible.md +20 -6
- package/docs/design-system/components/command.md +1 -1
- package/docs/design-system/components/context-menu.md +1 -1
- package/docs/design-system/components/date-picker.md +1 -1
- package/docs/design-system/components/file-upload.md +1 -1
- package/docs/design-system/components/hover-card.md +1 -1
- package/docs/design-system/components/inline-citations.md +93 -0
- package/docs/design-system/components/input-group.md +1 -1
- package/docs/design-system/components/input.md +59 -3
- package/docs/design-system/components/item.md +1 -1
- package/docs/design-system/components/kbd.md +6 -4
- package/docs/design-system/components/menu.md +35 -4
- package/docs/design-system/components/menubar.md +1 -1
- package/docs/design-system/components/modal.md +2 -2
- package/docs/design-system/components/native-select.md +1 -1
- package/docs/design-system/components/navigation-menu.md +1 -1
- package/docs/design-system/components/onboarding-step-list-item.md +1 -1
- package/docs/design-system/components/page-header.md +58 -17
- package/docs/design-system/components/pagination.md +2 -2
- package/docs/design-system/components/popover.md +1 -1
- package/docs/design-system/components/progress-slider.md +1 -1
- package/docs/design-system/components/radio-card.md +1 -1
- package/docs/design-system/components/radio-group.md +1 -1
- package/docs/design-system/components/resizable.md +1 -1
- package/docs/design-system/components/rolling-count.md +61 -0
- package/docs/design-system/components/scroll-area.md +1 -1
- package/docs/design-system/components/select.md +12 -2
- package/docs/design-system/components/sheet.md +1 -1
- package/docs/design-system/components/shimmer-text.md +67 -0
- package/docs/design-system/components/sidebar.md +75 -8
- package/docs/design-system/components/skeleton.md +2 -2
- package/docs/design-system/components/slideout.md +1 -1
- package/docs/design-system/components/slider.md +2 -2
- package/docs/design-system/components/social-button.md +2 -2
- package/docs/design-system/components/streaming-text.md +82 -0
- package/docs/design-system/components/table.md +103 -21
- package/docs/design-system/components/tabs.md +25 -6
- package/docs/design-system/components/tag.md +1 -1
- package/docs/design-system/components/task-list.md +95 -0
- package/docs/design-system/components/textarea.md +1 -1
- package/docs/design-system/components/thinking-reasoning.md +72 -0
- package/docs/design-system/components/thinking-state.md +67 -0
- package/docs/design-system/components/timeline.md +1 -1
- package/docs/design-system/components/toast.md +1 -1
- package/docs/design-system/components/toggle.md +2 -2
- package/docs/design-system/components/toolbar.md +11 -6
- package/docs/design-system/components/tooltip.md +6 -4
- package/docs/design-system/density-and-elevation.md +16 -16
- package/docs/design-system/design-system-test-missing-items.md +8 -8
- package/docs/design-system/installation.md +2 -0
- package/docs/design-system/tabs.md +2 -1
- package/docs/design-system/template-gap-audit.md +32 -0
- package/docs/design-system/templates.md +59 -0
- package/docs/design-system/token-governance.md +15 -2
- package/package.json +2 -2
- package/registry/components/accordion.tsx +2 -2
- package/registry/components/agent-input-parts.tsx +552 -0
- package/registry/components/agent-input-state.ts +156 -0
- package/registry/components/agent-input.css +307 -0
- package/registry/components/agent-input.tsx +382 -0
- package/registry/components/alert-dialog.tsx +2 -2
- package/registry/components/alert.tsx +23 -7
- package/registry/components/app-header.tsx +151 -0
- package/registry/components/approval-card.css +409 -0
- package/registry/components/approval-card.tsx +401 -0
- package/registry/components/approval-frame.tsx +108 -0
- package/registry/components/approval-questions.tsx +559 -0
- package/registry/components/attribute.tsx +48 -13
- package/registry/components/autocomplete.tsx +15 -68
- package/registry/components/avatar.tsx +2 -2
- package/registry/components/badge.tsx +8 -7
- package/registry/components/button-group.tsx +2 -2
- package/registry/components/button.tsx +19 -14
- package/registry/components/card.tsx +18 -5
- package/registry/components/chart-block-data.ts +177 -0
- package/registry/components/chart-block.css +171 -0
- package/registry/components/chart-block.tsx +260 -0
- package/registry/components/chart-interactions.ts +123 -0
- package/registry/components/chart-options.cartesian.ts +162 -0
- package/registry/components/chart-options.paint.ts +99 -0
- package/registry/components/chart-options.parts.ts +72 -0
- package/registry/components/chart-options.radial.ts +109 -0
- package/registry/components/chart-options.ts +44 -0
- package/registry/components/chart-options.types.ts +67 -0
- package/registry/components/chart.tsx +1 -1
- package/registry/components/checkbox.tsx +6 -6
- package/registry/components/collapsible.tsx +43 -3
- package/registry/components/command.tsx +1 -1
- package/registry/components/context-menu.tsx +1 -1
- package/registry/components/data-chart.tsx +40 -0
- package/registry/components/date-picker.tsx +9 -9
- package/registry/components/echart-theme.ts +246 -0
- package/registry/components/echart.css +49 -0
- package/registry/components/echart.tsx +178 -0
- package/registry/components/expanded/Breadcrumbs.tsx +5 -1
- package/registry/components/expanded/PageHeader.tsx +67 -24
- package/registry/components/expanded/Table.tsx +60 -22
- package/registry/components/expanded/activityFeed.css +2 -2
- package/registry/components/expanded/activityHeatmap.css +1 -1
- package/registry/components/expanded/banner.css +2 -2
- package/registry/components/expanded/breadcrumbs.css +19 -2
- package/registry/components/expanded/catalogComponentsShowcase.css +1 -1
- package/registry/components/expanded/fileUpload.css +7 -7
- package/registry/components/expanded/onboardingStepListItem.css +2 -2
- package/registry/components/expanded/pageHeader.css +38 -44
- package/registry/components/expanded/slideout.css +4 -4
- package/registry/components/expanded/table.css +58 -9
- package/registry/components/expanded/timeline.css +1 -1
- package/registry/components/hover-card.tsx +1 -1
- package/registry/components/inline-citations.css +126 -0
- package/registry/components/inline-citations.tsx +292 -0
- package/registry/components/input-group.tsx +4 -4
- package/registry/components/input.tsx +150 -137
- package/registry/components/item.tsx +1 -1
- package/registry/components/kbd.tsx +3 -2
- package/registry/components/menu.tsx +117 -12
- package/registry/components/menubar.tsx +2 -2
- package/registry/components/modal.tsx +3 -3
- package/registry/components/native-select.tsx +4 -4
- package/registry/components/navigation-menu.tsx +2 -2
- package/registry/components/pagination.tsx +4 -4
- package/registry/components/popover.tsx +3 -3
- package/registry/components/progress-slider.tsx +1 -1
- package/registry/components/radio-card.tsx +7 -7
- package/registry/components/radio-group.tsx +2 -2
- package/registry/components/resizable.tsx +2 -2
- package/registry/components/rolling-count.css +37 -0
- package/registry/components/rolling-count.tsx +110 -0
- package/registry/components/scroll-area.tsx +1 -1
- package/registry/components/select.tsx +15 -10
- package/registry/components/sheet.tsx +2 -2
- package/registry/components/shimmer-text.css +69 -0
- package/registry/components/shimmer-text.tsx +43 -0
- package/registry/components/sidebar.tsx +528 -223
- package/registry/components/skeleton.tsx +1 -1
- package/registry/components/slider.tsx +2 -2
- package/registry/components/social-button.tsx +1 -1
- package/registry/components/streaming-text.css +45 -0
- package/registry/components/streaming-text.tsx +165 -0
- package/registry/components/table-elements.tsx +164 -5
- package/registry/components/tabs.css +32 -4
- package/registry/components/tabs.tsx +5 -1
- package/registry/components/tag.tsx +1 -1
- package/registry/components/task-list.css +328 -0
- package/registry/components/task-list.tsx +429 -0
- package/registry/components/textarea.tsx +4 -4
- package/registry/components/thinking-reasoning.css +178 -0
- package/registry/components/thinking-reasoning.tsx +253 -0
- package/registry/components/thinking-state.css +8 -0
- package/registry/components/thinking-state.tsx +46 -0
- package/registry/components/toast.tsx +1 -1
- package/registry/components/toggle.tsx +2 -2
- package/registry/components/toolbar.tsx +6 -6
- package/registry/components/tooltip.tsx +6 -4
- package/registry/examples/agent-input-demo.tsx +90 -0
- package/registry/examples/app-header-demo.tsx +40 -0
- package/registry/examples/approval-card-demo.tsx +83 -0
- package/registry/examples/button-demo.tsx +3 -0
- package/registry/examples/chart-block-demo.tsx +5 -0
- package/registry/examples/chart-demo.tsx +6 -21
- package/registry/examples/collection-filter-bar-demo.tsx +1 -1
- package/registry/examples/control-density-contract-demo.tsx +5 -1
- package/registry/examples/inline-citations-demo.tsx +31 -0
- package/registry/examples/input-actions-demo.tsx +36 -0
- package/registry/examples/menu-demo.tsx +1 -1
- package/registry/examples/popover-demo.tsx +1 -0
- package/registry/examples/rolling-count-demo.tsx +39 -0
- package/registry/examples/shimmer-text-demo.tsx +20 -0
- package/registry/examples/streaming-text-demo.tsx +28 -0
- package/registry/examples/task-list-demo.tsx +74 -0
- package/registry/examples/thinking-reasoning-demo.tsx +74 -0
- package/registry/examples/thinking-state-demo.tsx +13 -0
- package/registry/guidance/charts.LICENSE.txt +24 -0
- package/registry/hooks/use-controllable-state.ts +33 -0
- package/registry/hooks/use-listbox-navigation.ts +73 -0
- package/registry/hooks/use-motion-end.ts +55 -0
- package/registry/hooks/use-reduced-motion.ts +28 -0
- package/registry/index.json +1808 -190
- package/registry/styles/globals.css +992 -1088
- package/registry.json +385 -10
- package/schema/registry.schema.json +5 -1
- package/schema/tokens.schema.json +8 -4
- package/src/cli/file-transaction.js +15 -8
- package/src/cli/index.js +22 -29
- package/tokens/banhaten.tokens.json +490 -192
|
@@ -15,7 +15,7 @@ const accordionVariants = cva("w-full", {
|
|
|
15
15
|
bordered:
|
|
16
16
|
"rounded-[var(--bh-radius-md-6)] border border-[var(--bh-border-subtle)] bg-[var(--bh-bg-raised)] [&>[data-slot=accordion-item]]:border-b-[var(--bh-space-xxxs-1)] [&>[data-slot=accordion-item]]:border-[var(--bh-border-subtle)] [&>[data-slot=accordion-item]:last-child]:border-b-0 [&_[data-slot=accordion-content]>div]:px-[var(--bh-space-3xl-16)] [&_[data-slot=accordion-trigger]]:px-[var(--bh-space-3xl-16)]",
|
|
17
17
|
cards:
|
|
18
|
-
"flex flex-col gap-[var(--bh-space-md-8)] [&>[data-slot=accordion-item]]:rounded-[var(--bh-radius-md-6)] [&>[data-slot=accordion-item]]:border [&>[data-slot=accordion-item]]:border-[var(--bh-border-subtle)] [&>[data-slot=accordion-item]]:bg-[var(--bh-bg-raised)] [&>[data-slot=accordion-item]]:shadow-[var(--
|
|
18
|
+
"flex flex-col gap-[var(--bh-space-md-8)] [&>[data-slot=accordion-item]]:rounded-[var(--bh-radius-md-6)] [&>[data-slot=accordion-item]]:border [&>[data-slot=accordion-item]]:border-[var(--bh-border-subtle)] [&>[data-slot=accordion-item]]:bg-[var(--bh-bg-raised)] [&>[data-slot=accordion-item]]:shadow-[var(--elevation-accordion-item)] [&_[data-slot=accordion-content]>div]:px-[var(--bh-space-3xl-16)] [&_[data-slot=accordion-trigger]]:px-[var(--bh-space-3xl-16)]",
|
|
19
19
|
soft: "rounded-[var(--bh-radius-md-6)] bg-[var(--bh-bg-neutral-subtle)] p-[var(--bh-space-xs-4)] [&>[data-slot=accordion-item]]:rounded-[var(--bh-radius-xs-2)] [&_[data-slot=accordion-content]>div]:px-[var(--bh-space-xl-12)] [&_[data-slot=accordion-trigger]]:px-[var(--bh-space-xl-12)]",
|
|
20
20
|
},
|
|
21
21
|
size: {
|
|
@@ -78,7 +78,7 @@ function AccordionTrigger({
|
|
|
78
78
|
dir={triggerDirection}
|
|
79
79
|
data-slot="accordion-trigger"
|
|
80
80
|
className={cn(
|
|
81
|
-
"flex flex-1 items-start justify-between gap-[var(--bh-space-3xl-16)] rounded-[var(--bh-radius-md-6)] py-[var(--bh-space-3xl-16)] text-start text-[length:var(--bh-text-body-sm-medium-font-size)] font-[var(--bh-text-body-sm-medium-font-weight)] leading-[var(--bh-text-body-sm-medium-line-height)] tracking-[var(--bh-text-body-sm-medium-letter-spacing)] text-[var(--bh-content-default)] outline-none transition-[background-color,color,box-shadow] hover:text-[var(--bh-content-default)] focus-visible:shadow-[var(--
|
|
81
|
+
"flex flex-1 items-start justify-between gap-[var(--bh-space-3xl-16)] rounded-[var(--bh-radius-md-6)] py-[var(--bh-space-3xl-16)] text-start text-[length:var(--bh-text-body-sm-medium-font-size)] font-[var(--bh-text-body-sm-medium-font-weight)] leading-[var(--bh-text-body-sm-medium-line-height)] tracking-[var(--bh-text-body-sm-medium-letter-spacing)] text-[var(--bh-content-default)] outline-none transition-[background-color,color,box-shadow] hover:text-[var(--bh-content-default)] focus-visible:shadow-[var(--elevation-button-focus)] disabled:pointer-events-none disabled:opacity-[var(--bh-opacity-50)] data-[disabled]:pointer-events-none data-[disabled]:opacity-[var(--bh-opacity-50)] [&[data-state=open]>svg]:rotate-180 [&_svg]:size-[var(--bh-space-3xl-16)]",
|
|
82
82
|
className
|
|
83
83
|
)}
|
|
84
84
|
{...props}
|
|
@@ -0,0 +1,552 @@
|
|
|
1
|
+
"use client"
|
|
2
|
+
|
|
3
|
+
import * as React from "react"
|
|
4
|
+
import {
|
|
5
|
+
BookOpenIcon,
|
|
6
|
+
CheckIcon,
|
|
7
|
+
ImageIcon,
|
|
8
|
+
PaperclipIcon,
|
|
9
|
+
PlusIcon,
|
|
10
|
+
SlashIcon,
|
|
11
|
+
} from "lucide-react"
|
|
12
|
+
|
|
13
|
+
import { Button } from "@/components/ui/button"
|
|
14
|
+
import {
|
|
15
|
+
Menu,
|
|
16
|
+
MenuContent,
|
|
17
|
+
MenuItem,
|
|
18
|
+
MenuItemDescription,
|
|
19
|
+
MenuItemIcon,
|
|
20
|
+
MenuItemText,
|
|
21
|
+
MenuItemTitle,
|
|
22
|
+
MenuLabel,
|
|
23
|
+
MenuSeparator,
|
|
24
|
+
MenuSub,
|
|
25
|
+
MenuSubContent,
|
|
26
|
+
MenuSubTrigger,
|
|
27
|
+
MenuTrigger,
|
|
28
|
+
} from "@/components/ui/menu"
|
|
29
|
+
import { Tag } from "@/components/ui/tag"
|
|
30
|
+
import { useListboxNavigation } from "@/hooks/use-listbox-navigation"
|
|
31
|
+
import { useMotionEnd, waitForMotion } from "@/hooks/use-motion-end"
|
|
32
|
+
import { useReducedMotion } from "@/hooks/use-reduced-motion"
|
|
33
|
+
|
|
34
|
+
import {
|
|
35
|
+
findSlashQuery,
|
|
36
|
+
matchSkills,
|
|
37
|
+
settleLeavingChip,
|
|
38
|
+
withLeavingChips,
|
|
39
|
+
type AgentInputLabels,
|
|
40
|
+
type AgentInputModel,
|
|
41
|
+
type AgentInputPhase,
|
|
42
|
+
type AgentInputSkill,
|
|
43
|
+
type AttachmentKind,
|
|
44
|
+
type Chip,
|
|
45
|
+
type ChipEntry,
|
|
46
|
+
type LeavingChip,
|
|
47
|
+
type SlashToken,
|
|
48
|
+
} from "./agent-input-state"
|
|
49
|
+
|
|
50
|
+
/* ------------------------------------------------------------------ Chips */
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Wraps one chip. A removed chip keeps its slot until its exit animation
|
|
54
|
+
* ends, then reports `onExited`; without a running animation it reports at
|
|
55
|
+
* once, so a removed chip never lingers.
|
|
56
|
+
*/
|
|
57
|
+
function ChipSlot({
|
|
58
|
+
children,
|
|
59
|
+
exiting,
|
|
60
|
+
onExited,
|
|
61
|
+
}: {
|
|
62
|
+
children: React.ReactNode
|
|
63
|
+
exiting: boolean
|
|
64
|
+
onExited: () => void
|
|
65
|
+
}) {
|
|
66
|
+
const ref = React.useRef<HTMLSpanElement>(null)
|
|
67
|
+
useMotionEnd(ref, exiting, onExited)
|
|
68
|
+
|
|
69
|
+
return (
|
|
70
|
+
<span data-exit={exiting ? "true" : "false"} data-slot="agent-input-chip" ref={ref}>
|
|
71
|
+
{children}
|
|
72
|
+
</span>
|
|
73
|
+
)
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* The chips of the current value, plus copies of removed ones kept where they
|
|
78
|
+
* were until their exit animation ends. The value drops a chip at once and
|
|
79
|
+
* only its exit lingers, so quick removals and host updates in the meantime
|
|
80
|
+
* are never undone.
|
|
81
|
+
*/
|
|
82
|
+
function useChipPresence(live: readonly Chip[]) {
|
|
83
|
+
const [leaving, setLeaving] = React.useState<readonly LeavingChip[]>([])
|
|
84
|
+
const liveKeys = new Set(live.map((chip) => chip.key))
|
|
85
|
+
|
|
86
|
+
// A chip added back while its old copy was still leaving replaces it.
|
|
87
|
+
if (leaving.some(({ chip }) => liveKeys.has(chip.key))) {
|
|
88
|
+
setLeaving(leaving.filter(({ chip }) => !liveKeys.has(chip.key)))
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const chips = withLeavingChips(live, leaving)
|
|
92
|
+
|
|
93
|
+
/** Keeps a live chip on screen while it leaves; returns it, or null. */
|
|
94
|
+
const leave = (key: string) => {
|
|
95
|
+
const index = chips.findIndex(({ chip }) => chip.key === key)
|
|
96
|
+
const entry = chips[index]
|
|
97
|
+
if (!entry || entry.exiting) return null
|
|
98
|
+
setLeaving((current) => [...current, { chip: entry.chip, index }])
|
|
99
|
+
return entry.chip
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
const settle = (key: string) => setLeaving((current) => settleLeavingChip(current, key))
|
|
103
|
+
|
|
104
|
+
return { chips, leave, settle }
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
type ChipRowProps = {
|
|
108
|
+
chips: readonly ChipEntry[]
|
|
109
|
+
disabled: boolean
|
|
110
|
+
labels: AgentInputLabels
|
|
111
|
+
onExited: (key: string) => void
|
|
112
|
+
onRemove: (key: string) => void
|
|
113
|
+
skillName: (id: string) => string
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function ChipRow({ chips, disabled, labels, onExited, onRemove, skillName }: ChipRowProps) {
|
|
117
|
+
if (chips.length === 0) return null
|
|
118
|
+
|
|
119
|
+
return (
|
|
120
|
+
<div data-slot="agent-input-chips">
|
|
121
|
+
{chips.map(({ chip, exiting }) => (
|
|
122
|
+
<ChipSlot exiting={exiting} key={chip.key} onExited={() => onExited(chip.key)}>
|
|
123
|
+
{chip.kind === "skill" ? (
|
|
124
|
+
<Tag
|
|
125
|
+
closeLabel={labels.removeSkill(skillName(chip.skillId))}
|
|
126
|
+
disabled={disabled}
|
|
127
|
+
icon={<SlashIcon />}
|
|
128
|
+
onClose={() => onRemove(chip.key)}
|
|
129
|
+
showCloseButton
|
|
130
|
+
size="xs"
|
|
131
|
+
state="active"
|
|
132
|
+
type="icon"
|
|
133
|
+
>
|
|
134
|
+
{skillName(chip.skillId)}
|
|
135
|
+
</Tag>
|
|
136
|
+
) : (
|
|
137
|
+
<Tag
|
|
138
|
+
closeLabel={labels.removeAttachment(chip.attachment.name)}
|
|
139
|
+
disabled={disabled}
|
|
140
|
+
icon={chip.attachment.kind === "image" ? <ImageIcon /> : <PaperclipIcon />}
|
|
141
|
+
onClose={() => onRemove(chip.key)}
|
|
142
|
+
showCloseButton
|
|
143
|
+
size="xs"
|
|
144
|
+
type="icon"
|
|
145
|
+
>
|
|
146
|
+
{chip.attachment.name}
|
|
147
|
+
</Tag>
|
|
148
|
+
)}
|
|
149
|
+
</ChipSlot>
|
|
150
|
+
))}
|
|
151
|
+
</div>
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/* ---------------------------------------------------------- Slash palette */
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Opens when a "/" sits before the caret, filters skills by what follows it,
|
|
159
|
+
* and remembers that token so picking a skill can take it out of the text.
|
|
160
|
+
*/
|
|
161
|
+
function useSlashPalette(skills: readonly AgentInputSkill[]) {
|
|
162
|
+
const [open, setOpen] = React.useState(false)
|
|
163
|
+
const [query, setQuery] = React.useState("")
|
|
164
|
+
// True while the arrows drive the list, so a resting pointer does not steal
|
|
165
|
+
// the active option.
|
|
166
|
+
const [keyboard, setKeyboard] = React.useState(false)
|
|
167
|
+
const token = React.useRef<SlashToken | null>(null)
|
|
168
|
+
const id = React.useId()
|
|
169
|
+
const results = React.useMemo(() => matchSkills(skills, query), [skills, query])
|
|
170
|
+
const nav = useListboxNavigation({ idPrefix: id, open, options: results })
|
|
171
|
+
|
|
172
|
+
const close = () => {
|
|
173
|
+
setOpen(false)
|
|
174
|
+
setQuery("")
|
|
175
|
+
setKeyboard(false)
|
|
176
|
+
token.current = null
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const detect = (text: string, caret: number | null) => {
|
|
180
|
+
if (caret === null || skills.length === 0) return close()
|
|
181
|
+
const found = findSlashQuery(text, caret)
|
|
182
|
+
if (!found) return close()
|
|
183
|
+
token.current = { end: found.end, start: found.start }
|
|
184
|
+
// A new query starts again from its first match.
|
|
185
|
+
if (found.query !== query) nav.setActiveIndex(0)
|
|
186
|
+
setQuery(found.query)
|
|
187
|
+
setOpen(true)
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/** Moves, picks, or closes from the keyboard; returns whether it used the key. */
|
|
191
|
+
const handleKey = (
|
|
192
|
+
event: React.KeyboardEvent,
|
|
193
|
+
pick: (skill: AgentInputSkill) => void
|
|
194
|
+
) => {
|
|
195
|
+
if (!open) return false
|
|
196
|
+
if (results.length > 0 && (event.key === "ArrowDown" || event.key === "ArrowUp")) {
|
|
197
|
+
event.preventDefault()
|
|
198
|
+
setKeyboard(true)
|
|
199
|
+
nav.move(event.key === "ArrowDown" ? 1 : -1)
|
|
200
|
+
return true
|
|
201
|
+
}
|
|
202
|
+
if (results.length > 0 && (event.key === "Enter" || event.key === "Tab")) {
|
|
203
|
+
event.preventDefault()
|
|
204
|
+
pick(results[Math.max(nav.activeIndex, 0)])
|
|
205
|
+
return true
|
|
206
|
+
}
|
|
207
|
+
if (event.key === "Escape") {
|
|
208
|
+
event.preventDefault()
|
|
209
|
+
close()
|
|
210
|
+
return true
|
|
211
|
+
}
|
|
212
|
+
return false
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
return { close, detect, handleKey, id, keyboard, nav, open, results, setKeyboard, token }
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
type SlashPaletteState = ReturnType<typeof useSlashPalette>
|
|
219
|
+
|
|
220
|
+
function SlashPalette({
|
|
221
|
+
labels,
|
|
222
|
+
onPick,
|
|
223
|
+
slash,
|
|
224
|
+
}: {
|
|
225
|
+
labels: AgentInputLabels
|
|
226
|
+
onPick: (skill: AgentInputSkill) => void
|
|
227
|
+
slash: SlashPaletteState
|
|
228
|
+
}) {
|
|
229
|
+
return (
|
|
230
|
+
<div
|
|
231
|
+
aria-label={labels.skills}
|
|
232
|
+
data-keyboard={slash.keyboard ? "true" : "false"}
|
|
233
|
+
data-slot="agent-input-palette"
|
|
234
|
+
id={slash.id}
|
|
235
|
+
onMouseMove={() => {
|
|
236
|
+
if (slash.keyboard) slash.setKeyboard(false)
|
|
237
|
+
}}
|
|
238
|
+
role="listbox"
|
|
239
|
+
>
|
|
240
|
+
<div aria-hidden="true" data-slot="agent-input-palette-label">
|
|
241
|
+
{labels.skills}
|
|
242
|
+
</div>
|
|
243
|
+
{slash.results.length > 0 ? (
|
|
244
|
+
slash.results.map((skill, index) => (
|
|
245
|
+
<button
|
|
246
|
+
aria-selected={index === slash.nav.activeIndex}
|
|
247
|
+
data-active={index === slash.nav.activeIndex ? "true" : "false"}
|
|
248
|
+
data-slot="agent-input-palette-item"
|
|
249
|
+
id={slash.nav.optionId(index)}
|
|
250
|
+
key={skill.id}
|
|
251
|
+
onClick={() => onPick(skill)}
|
|
252
|
+
onMouseDown={(event) => event.preventDefault()}
|
|
253
|
+
onMouseEnter={() => {
|
|
254
|
+
if (!slash.keyboard) slash.nav.setActiveIndex(index)
|
|
255
|
+
}}
|
|
256
|
+
role="option"
|
|
257
|
+
tabIndex={-1}
|
|
258
|
+
type="button"
|
|
259
|
+
>
|
|
260
|
+
{skill.icon ? <span data-slot="agent-input-palette-icon">{skill.icon}</span> : null}
|
|
261
|
+
<span data-slot="agent-input-palette-name" dir="auto">
|
|
262
|
+
{skill.name}
|
|
263
|
+
</span>
|
|
264
|
+
</button>
|
|
265
|
+
))
|
|
266
|
+
) : (
|
|
267
|
+
<div data-slot="agent-input-palette-empty">{labels.noSkills}</div>
|
|
268
|
+
)}
|
|
269
|
+
</div>
|
|
270
|
+
)
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
/* ------------------------------------------------------------ Enhancement */
|
|
274
|
+
|
|
275
|
+
/**
|
|
276
|
+
* Rewrites the prompt through `onEnhance`: a failure restores the original,
|
|
277
|
+
* revert brings it back, and unmounting aborts the request. The frame eases
|
|
278
|
+
* between its old and new height when the text changes size.
|
|
279
|
+
*/
|
|
280
|
+
function usePromptEnhancement({
|
|
281
|
+
frameRef,
|
|
282
|
+
onEnhance,
|
|
283
|
+
onFinish,
|
|
284
|
+
onStart,
|
|
285
|
+
setValue,
|
|
286
|
+
value,
|
|
287
|
+
}: {
|
|
288
|
+
frameRef: React.RefObject<HTMLDivElement | null>
|
|
289
|
+
onEnhance?: (prompt: string, signal: AbortSignal) => Promise<string>
|
|
290
|
+
/** Runs once a request settles, unless the field unmounted meanwhile. */
|
|
291
|
+
onFinish: () => void
|
|
292
|
+
onStart: () => void
|
|
293
|
+
setValue: (value: string) => void
|
|
294
|
+
value: string
|
|
295
|
+
}) {
|
|
296
|
+
const [phase, setPhase] = React.useState<AgentInputPhase>("idle")
|
|
297
|
+
const original = React.useRef("")
|
|
298
|
+
const request = React.useRef<AbortController | null>(null)
|
|
299
|
+
const flipFrom = React.useRef<number | null>(null)
|
|
300
|
+
const reducedMotion = useReducedMotion()
|
|
301
|
+
const enhancing = phase === "enhancing"
|
|
302
|
+
|
|
303
|
+
React.useLayoutEffect(() => {
|
|
304
|
+
const frame = frameRef.current
|
|
305
|
+
const from = flipFrom.current
|
|
306
|
+
flipFrom.current = null
|
|
307
|
+
if (!frame || from === null || enhancing || reducedMotion) return
|
|
308
|
+
const to = frame.offsetHeight
|
|
309
|
+
if (from === to) return
|
|
310
|
+
frame.style.height = `${from}px`
|
|
311
|
+
frame.style.overflow = "hidden"
|
|
312
|
+
void frame.offsetHeight
|
|
313
|
+
frame.style.transition = "height var(--bh-duration-default) var(--bh-easing-emphasized)"
|
|
314
|
+
frame.style.height = `${to}px`
|
|
315
|
+
// Never cancelled: however the next render goes, the frame must drop
|
|
316
|
+
// its fixed height once the transition is over.
|
|
317
|
+
waitForMotion(frame, () => {
|
|
318
|
+
frame.style.transition = ""
|
|
319
|
+
frame.style.height = ""
|
|
320
|
+
frame.style.overflow = ""
|
|
321
|
+
})
|
|
322
|
+
}, [enhancing, frameRef, phase, reducedMotion])
|
|
323
|
+
|
|
324
|
+
React.useEffect(() => () => request.current?.abort(), [])
|
|
325
|
+
|
|
326
|
+
const run = async () => {
|
|
327
|
+
if (!onEnhance || !value.trim() || enhancing) return
|
|
328
|
+
original.current = value
|
|
329
|
+
setPhase("enhancing")
|
|
330
|
+
onStart()
|
|
331
|
+
const controller = new AbortController()
|
|
332
|
+
request.current = controller
|
|
333
|
+
try {
|
|
334
|
+
const result = await onEnhance(value, controller.signal)
|
|
335
|
+
if (controller.signal.aborted) return
|
|
336
|
+
flipFrom.current = frameRef.current?.offsetHeight ?? null
|
|
337
|
+
setValue(result)
|
|
338
|
+
setPhase("enhanced")
|
|
339
|
+
} catch {
|
|
340
|
+
if (controller.signal.aborted) return
|
|
341
|
+
setValue(original.current)
|
|
342
|
+
setPhase("idle")
|
|
343
|
+
}
|
|
344
|
+
onFinish()
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const revert = () => {
|
|
348
|
+
request.current?.abort()
|
|
349
|
+
flipFrom.current = frameRef.current?.offsetHeight ?? null
|
|
350
|
+
setValue(original.current)
|
|
351
|
+
setPhase("idle")
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
/** Editing or sending leaves the enhanced prompt behind. */
|
|
355
|
+
const reset = () => setPhase("idle")
|
|
356
|
+
|
|
357
|
+
return { enhancing, phase, reset, revert, run }
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
/** Keeps the enhance pill mounted through its exit animation. */
|
|
361
|
+
function useEnhancePill(visible: boolean, enhancing: boolean) {
|
|
362
|
+
const ref = React.useRef<HTMLButtonElement>(null)
|
|
363
|
+
const [mounted, setMounted] = React.useState(false)
|
|
364
|
+
const [exiting, setExiting] = React.useState(false)
|
|
365
|
+
|
|
366
|
+
React.useEffect(() => {
|
|
367
|
+
if (visible) {
|
|
368
|
+
setMounted(true)
|
|
369
|
+
setExiting(false)
|
|
370
|
+
return
|
|
371
|
+
}
|
|
372
|
+
if (!mounted) return
|
|
373
|
+
// The spinner takes its place at once while a request runs.
|
|
374
|
+
if (enhancing) {
|
|
375
|
+
setMounted(false)
|
|
376
|
+
setExiting(false)
|
|
377
|
+
return
|
|
378
|
+
}
|
|
379
|
+
setExiting(true)
|
|
380
|
+
}, [enhancing, mounted, visible])
|
|
381
|
+
|
|
382
|
+
useMotionEnd(ref, exiting, () => {
|
|
383
|
+
setMounted(false)
|
|
384
|
+
setExiting(false)
|
|
385
|
+
})
|
|
386
|
+
|
|
387
|
+
return { exiting, mounted, ref }
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/* --------------------------------------------------------------- Add menu */
|
|
391
|
+
|
|
392
|
+
/** A hidden multi-file input that the add menu opens as a photo or file picker. */
|
|
393
|
+
function useFilePicker(onPick: (files: File[], picker: AttachmentKind) => void) {
|
|
394
|
+
const ref = React.useRef<HTMLInputElement>(null)
|
|
395
|
+
|
|
396
|
+
const open = (kind: AttachmentKind) => {
|
|
397
|
+
const input = ref.current
|
|
398
|
+
if (!input) return
|
|
399
|
+
input.accept = kind === "image" ? "image/*" : ""
|
|
400
|
+
input.dataset.kind = kind
|
|
401
|
+
input.value = ""
|
|
402
|
+
input.click()
|
|
403
|
+
}
|
|
404
|
+
|
|
405
|
+
const onChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
406
|
+
const files = Array.from(event.target.files ?? [])
|
|
407
|
+
if (files.length === 0) return
|
|
408
|
+
onPick(files, (event.target.dataset.kind as AttachmentKind | undefined) ?? "file")
|
|
409
|
+
event.target.value = ""
|
|
410
|
+
}
|
|
411
|
+
|
|
412
|
+
return {
|
|
413
|
+
inputProps: { hidden: true, multiple: true, onChange, ref, tabIndex: -1, type: "file" },
|
|
414
|
+
open,
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
type AddMenuProps = {
|
|
419
|
+
disabled: boolean
|
|
420
|
+
labels: AgentInputLabels
|
|
421
|
+
model?: string
|
|
422
|
+
models: readonly AgentInputModel[]
|
|
423
|
+
onModelChange: (model: string) => void
|
|
424
|
+
onPickFiles: (kind: AttachmentKind) => void
|
|
425
|
+
onToggleSkill: (skillId: string) => void
|
|
426
|
+
selectedSkills: readonly string[]
|
|
427
|
+
skills: readonly AgentInputSkill[]
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/** Photos, files, skills, and the model, behind the plus button. */
|
|
431
|
+
function AddMenu({
|
|
432
|
+
disabled,
|
|
433
|
+
labels,
|
|
434
|
+
model,
|
|
435
|
+
models,
|
|
436
|
+
onModelChange,
|
|
437
|
+
onPickFiles,
|
|
438
|
+
onToggleSkill,
|
|
439
|
+
selectedSkills,
|
|
440
|
+
skills,
|
|
441
|
+
}: AddMenuProps) {
|
|
442
|
+
const [open, setOpen] = React.useState(false)
|
|
443
|
+
|
|
444
|
+
return (
|
|
445
|
+
<Menu onOpenChange={setOpen} open={open}>
|
|
446
|
+
<MenuTrigger asChild>
|
|
447
|
+
<Button
|
|
448
|
+
aria-label={labels.addMenu}
|
|
449
|
+
data-open={open ? "true" : "false"}
|
|
450
|
+
data-slot="agent-input-add"
|
|
451
|
+
disabled={disabled}
|
|
452
|
+
size="icon-xs"
|
|
453
|
+
variant="soft"
|
|
454
|
+
>
|
|
455
|
+
<PlusIcon data-slot="agent-input-add-icon" />
|
|
456
|
+
</Button>
|
|
457
|
+
</MenuTrigger>
|
|
458
|
+
<MenuContent align="start" side="top">
|
|
459
|
+
<MenuItem onSelect={() => onPickFiles("image")}>
|
|
460
|
+
<MenuItemIcon>
|
|
461
|
+
<ImageIcon />
|
|
462
|
+
</MenuItemIcon>
|
|
463
|
+
{labels.addPhotos}
|
|
464
|
+
</MenuItem>
|
|
465
|
+
<MenuItem onSelect={() => onPickFiles("file")}>
|
|
466
|
+
<MenuItemIcon>
|
|
467
|
+
<PaperclipIcon />
|
|
468
|
+
</MenuItemIcon>
|
|
469
|
+
{labels.addFiles}
|
|
470
|
+
</MenuItem>
|
|
471
|
+
{skills.length > 0 ? (
|
|
472
|
+
<>
|
|
473
|
+
<MenuSeparator />
|
|
474
|
+
<MenuSub>
|
|
475
|
+
<MenuSubTrigger>
|
|
476
|
+
<MenuItemIcon>
|
|
477
|
+
<BookOpenIcon />
|
|
478
|
+
</MenuItemIcon>
|
|
479
|
+
{labels.skills}
|
|
480
|
+
</MenuSubTrigger>
|
|
481
|
+
<MenuSubContent>
|
|
482
|
+
{skills.map((skill) => {
|
|
483
|
+
const selected = selectedSkills.includes(skill.id)
|
|
484
|
+
return (
|
|
485
|
+
<MenuItem
|
|
486
|
+
aria-checked={selected}
|
|
487
|
+
key={skill.id}
|
|
488
|
+
onSelect={() => onToggleSkill(skill.id)}
|
|
489
|
+
role="menuitemcheckbox"
|
|
490
|
+
>
|
|
491
|
+
{skill.icon ? <MenuItemIcon>{skill.icon}</MenuItemIcon> : null}
|
|
492
|
+
{skill.name}
|
|
493
|
+
{selected ? (
|
|
494
|
+
<MenuItemIcon position="trailing">
|
|
495
|
+
<CheckIcon />
|
|
496
|
+
</MenuItemIcon>
|
|
497
|
+
) : null}
|
|
498
|
+
</MenuItem>
|
|
499
|
+
)
|
|
500
|
+
})}
|
|
501
|
+
</MenuSubContent>
|
|
502
|
+
</MenuSub>
|
|
503
|
+
</>
|
|
504
|
+
) : null}
|
|
505
|
+
{models.length > 0 ? (
|
|
506
|
+
<>
|
|
507
|
+
<MenuSeparator />
|
|
508
|
+
<MenuLabel label={labels.model} />
|
|
509
|
+
{models.map((item) => {
|
|
510
|
+
const selected = item.id === model
|
|
511
|
+
return (
|
|
512
|
+
<MenuItem
|
|
513
|
+
aria-checked={selected}
|
|
514
|
+
key={item.id}
|
|
515
|
+
kind={item.description ? "multiline" : "default"}
|
|
516
|
+
onSelect={() => onModelChange(item.id)}
|
|
517
|
+
role="menuitemcheckbox"
|
|
518
|
+
>
|
|
519
|
+
{item.icon ? <MenuItemIcon>{item.icon}</MenuItemIcon> : null}
|
|
520
|
+
<MenuItemText>
|
|
521
|
+
<MenuItemTitle>{item.name}</MenuItemTitle>
|
|
522
|
+
{item.description ? (
|
|
523
|
+
<MenuItemDescription>
|
|
524
|
+
{item.meta ? `${item.description} · ${item.meta}` : item.description}
|
|
525
|
+
</MenuItemDescription>
|
|
526
|
+
) : null}
|
|
527
|
+
</MenuItemText>
|
|
528
|
+
{selected ? (
|
|
529
|
+
<MenuItemIcon position="trailing">
|
|
530
|
+
<CheckIcon />
|
|
531
|
+
</MenuItemIcon>
|
|
532
|
+
) : null}
|
|
533
|
+
</MenuItem>
|
|
534
|
+
)
|
|
535
|
+
})}
|
|
536
|
+
</>
|
|
537
|
+
) : null}
|
|
538
|
+
</MenuContent>
|
|
539
|
+
</Menu>
|
|
540
|
+
)
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
export {
|
|
544
|
+
AddMenu,
|
|
545
|
+
ChipRow,
|
|
546
|
+
SlashPalette,
|
|
547
|
+
useChipPresence,
|
|
548
|
+
useEnhancePill,
|
|
549
|
+
useFilePicker,
|
|
550
|
+
usePromptEnhancement,
|
|
551
|
+
useSlashPalette,
|
|
552
|
+
}
|