@tangle-network/ui 11.36.0 → 11.38.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 +20 -0
- package/dist/code-presentation.d.ts +2 -0
- package/dist/code-presentation.js +2 -0
- package/dist/index.d.ts +5 -2
- package/dist/index.js +5 -2
- package/dist/lib/code-presentation.d.ts +92 -0
- package/dist/lib/code-presentation.js +437 -0
- package/dist/lib/surface-motion.js +34 -13
- package/dist/markdown/api-schema.d.ts +9 -0
- package/dist/markdown/api-schema.js +20 -0
- package/dist/markdown/code-block.d.ts +15 -7
- package/dist/markdown/code-block.js +120 -268
- package/dist/markdown.d.ts +4 -2
- package/dist/markdown.js +4 -2
- package/dist/primitives/button.js +2 -1
- package/dist/primitives/code-block.d.ts +1 -1
- package/dist/primitives/popover.js +11 -3
- package/dist/primitives/wizard.d.ts +30 -0
- package/dist/primitives/wizard.js +158 -0
- package/dist/primitives.d.ts +2 -1
- package/dist/primitives.js +2 -1
- package/package.json +7 -2
- package/src/code-presentation.ts +2 -0
- package/src/lib/code-presentation.ts +257 -0
- package/src/lib/surface-motion.tsx +22 -15
- package/src/markdown/api-presentation.test.tsx +63 -0
- package/src/markdown/api-schema.tsx +9 -0
- package/src/markdown/code-block.tsx +54 -185
- package/src/markdown/index.ts +2 -1
- package/src/primitives/button.tsx +2 -1
- package/src/primitives/code-block.tsx +1 -1
- package/src/primitives/index.ts +3 -0
- package/src/primitives/popover.tsx +6 -8
- package/src/primitives/wizard.stories.tsx +51 -0
- package/src/primitives/wizard.test.tsx +106 -0
- package/src/primitives/wizard.tsx +212 -0
- package/src/styles/presentation.css +8 -0
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
import { cn } from "../lib/utils.js";
|
|
2
|
+
import { Button } from "./button.js";
|
|
3
|
+
import * as React from "react";
|
|
4
|
+
import { Fragment as Fragment$1, jsx, jsxs } from "react/jsx-runtime";
|
|
5
|
+
import { Check, ChevronLeft } from "lucide-react";
|
|
6
|
+
//#region src/primitives/wizard.tsx
|
|
7
|
+
const WizardContext = React.createContext(null);
|
|
8
|
+
function useWizardContext(component) {
|
|
9
|
+
const context = React.useContext(WizardContext);
|
|
10
|
+
if (!context) throw new Error(`<${component}> must be used inside <Wizard>`);
|
|
11
|
+
return context;
|
|
12
|
+
}
|
|
13
|
+
function Wizard({ children, step, defaultStep = 0, onStepChange, onFinished, className, ...rest }) {
|
|
14
|
+
const steps = React.Children.toArray(children).filter(React.isValidElement);
|
|
15
|
+
const [internal, setInternal] = React.useState(defaultStep);
|
|
16
|
+
const current = step !== void 0 ? step : internal;
|
|
17
|
+
const total = steps.length;
|
|
18
|
+
const goTo = React.useCallback((target) => {
|
|
19
|
+
const bounded = Math.max(0, Math.min(target, total - 1));
|
|
20
|
+
if (bounded === current) return;
|
|
21
|
+
onStepChange?.(bounded);
|
|
22
|
+
if (step === void 0) setInternal(bounded);
|
|
23
|
+
}, [
|
|
24
|
+
current,
|
|
25
|
+
onStepChange,
|
|
26
|
+
step,
|
|
27
|
+
total
|
|
28
|
+
]);
|
|
29
|
+
const context = React.useMemo(() => ({
|
|
30
|
+
current,
|
|
31
|
+
total,
|
|
32
|
+
goTo,
|
|
33
|
+
next: () => goTo(current + 1),
|
|
34
|
+
back: () => goTo(current - 1),
|
|
35
|
+
finish: () => onFinished?.()
|
|
36
|
+
}), [
|
|
37
|
+
current,
|
|
38
|
+
goTo,
|
|
39
|
+
onFinished,
|
|
40
|
+
total
|
|
41
|
+
]);
|
|
42
|
+
const titles = steps.map((child) => child.props.title);
|
|
43
|
+
const activeTitle = titles[current] ?? "";
|
|
44
|
+
return /* @__PURE__ */ jsx(WizardContext.Provider, {
|
|
45
|
+
value: context,
|
|
46
|
+
children: /* @__PURE__ */ jsxs("div", {
|
|
47
|
+
className: cn("w-full", className),
|
|
48
|
+
...rest,
|
|
49
|
+
children: [
|
|
50
|
+
/* @__PURE__ */ jsx("ol", {
|
|
51
|
+
"aria-label": "Steps",
|
|
52
|
+
className: "hidden items-center gap-2 sm:flex",
|
|
53
|
+
children: titles.map((title, index) => {
|
|
54
|
+
const state = index < current ? "done" : index === current ? "active" : "future";
|
|
55
|
+
const marker = /* @__PURE__ */ jsxs(Fragment$1, { children: [/* @__PURE__ */ jsx("span", {
|
|
56
|
+
className: cn("flex size-6 shrink-0 items-center justify-center rounded-full text-xs font-medium", state === "active" && "bg-primary text-primary-foreground", state === "done" && "bg-primary/15 text-primary", state === "future" && "bg-muted text-muted-foreground"),
|
|
57
|
+
children: state === "done" ? /* @__PURE__ */ jsx(Check, {
|
|
58
|
+
className: "size-3.5",
|
|
59
|
+
"aria-hidden": true
|
|
60
|
+
}) : index + 1
|
|
61
|
+
}), /* @__PURE__ */ jsx("span", {
|
|
62
|
+
className: cn("text-sm", state === "active" && "font-medium text-foreground", state === "done" && "text-foreground", state === "future" && "text-muted-foreground"),
|
|
63
|
+
children: title
|
|
64
|
+
})] });
|
|
65
|
+
return /* @__PURE__ */ jsxs(React.Fragment, { children: [index > 0 && /* @__PURE__ */ jsx("li", {
|
|
66
|
+
"aria-hidden": true,
|
|
67
|
+
className: "h-px flex-1 bg-border"
|
|
68
|
+
}), /* @__PURE__ */ jsx("li", { children: state === "future" ? /* @__PURE__ */ jsx("span", {
|
|
69
|
+
"aria-disabled": "true",
|
|
70
|
+
className: "flex items-center gap-2 p-1 opacity-70",
|
|
71
|
+
children: marker
|
|
72
|
+
}) : /* @__PURE__ */ jsx("button", {
|
|
73
|
+
type: "button",
|
|
74
|
+
onClick: () => goTo(index),
|
|
75
|
+
className: "-m-1 flex items-center gap-2 rounded-md p-1 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring",
|
|
76
|
+
children: marker
|
|
77
|
+
}) })] }, `${index}-${title}`);
|
|
78
|
+
})
|
|
79
|
+
}),
|
|
80
|
+
/* @__PURE__ */ jsxs("div", {
|
|
81
|
+
className: "sm:hidden",
|
|
82
|
+
children: [/* @__PURE__ */ jsxs("p", {
|
|
83
|
+
className: "text-xs text-muted-foreground",
|
|
84
|
+
children: [
|
|
85
|
+
"Step ",
|
|
86
|
+
current + 1,
|
|
87
|
+
" of ",
|
|
88
|
+
total,
|
|
89
|
+
" · ",
|
|
90
|
+
/* @__PURE__ */ jsx("span", {
|
|
91
|
+
className: "font-medium text-foreground",
|
|
92
|
+
children: activeTitle
|
|
93
|
+
})
|
|
94
|
+
]
|
|
95
|
+
}), /* @__PURE__ */ jsx("div", {
|
|
96
|
+
className: "mt-2 h-1 overflow-hidden rounded-full bg-muted",
|
|
97
|
+
role: "progressbar",
|
|
98
|
+
"aria-valuemin": 1,
|
|
99
|
+
"aria-valuemax": total,
|
|
100
|
+
"aria-valuenow": current + 1,
|
|
101
|
+
"aria-label": `Step ${current + 1} of ${total}`,
|
|
102
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
103
|
+
className: "h-full rounded-full bg-primary transition-all",
|
|
104
|
+
style: { width: `${(current + 1) / total * 100}%` }
|
|
105
|
+
})
|
|
106
|
+
})]
|
|
107
|
+
}),
|
|
108
|
+
steps.map((child, index) => index === current ? /* @__PURE__ */ jsx(React.Fragment, { children: child }, index) : null)
|
|
109
|
+
]
|
|
110
|
+
})
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
function WizardStep({ title, children, nextLabel, onNext, nextDisabled, hideBack }) {
|
|
114
|
+
const { current, total, goTo, back, finish } = useWizardContext("Wizard.Step");
|
|
115
|
+
const [busy, setBusy] = React.useState(false);
|
|
116
|
+
const isLast = current === total - 1;
|
|
117
|
+
async function runNext() {
|
|
118
|
+
if (!onNext) {
|
|
119
|
+
if (isLast) finish();
|
|
120
|
+
else goTo(current + 1);
|
|
121
|
+
return;
|
|
122
|
+
}
|
|
123
|
+
setBusy(true);
|
|
124
|
+
try {
|
|
125
|
+
if (await onNext() === false) return;
|
|
126
|
+
if (isLast) finish();
|
|
127
|
+
else goTo(current + 1);
|
|
128
|
+
} catch {} finally {
|
|
129
|
+
setBusy(false);
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
return /* @__PURE__ */ jsxs("section", {
|
|
133
|
+
"aria-label": title,
|
|
134
|
+
className: "pt-6",
|
|
135
|
+
children: [children, (nextLabel || !hideBack) && /* @__PURE__ */ jsxs("div", {
|
|
136
|
+
className: "mt-6 flex items-center justify-between gap-2 border-t border-border pt-4",
|
|
137
|
+
children: [!hideBack && current > 0 ? /* @__PURE__ */ jsxs(Button, {
|
|
138
|
+
type: "button",
|
|
139
|
+
variant: "ghost",
|
|
140
|
+
onClick: back,
|
|
141
|
+
children: [/* @__PURE__ */ jsx(ChevronLeft, {
|
|
142
|
+
className: "size-4",
|
|
143
|
+
"aria-hidden": true
|
|
144
|
+
}), " Back"]
|
|
145
|
+
}) : /* @__PURE__ */ jsx("span", {}), nextLabel && /* @__PURE__ */ jsx(Button, {
|
|
146
|
+
type: "button",
|
|
147
|
+
className: "w-full sm:w-auto",
|
|
148
|
+
onClick: () => void runNext(),
|
|
149
|
+
disabled: nextDisabled,
|
|
150
|
+
loading: busy,
|
|
151
|
+
children: nextLabel
|
|
152
|
+
})]
|
|
153
|
+
})]
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
Wizard.Step = WizardStep;
|
|
157
|
+
//#endregion
|
|
158
|
+
export { Wizard, WizardStep };
|
package/dist/primitives.d.ts
CHANGED
|
@@ -50,7 +50,8 @@ import { Pagination, PaginationProps } from "./primitives/pagination.js";
|
|
|
50
50
|
import { StatusPill, StatusPillProps, StatusTone } from "./primitives/status-pill.js";
|
|
51
51
|
import { Tag, TagProps } from "./primitives/tag.js";
|
|
52
52
|
import { Chip, ChipProps } from "./primitives/chip.js";
|
|
53
|
+
import { Wizard, WizardProps, WizardStep, WizardStepProps } from "./primitives/wizard.js";
|
|
53
54
|
import { Metric, MetricProps, MetricStrip, MetricStripProps } from "./primitives/metric-strip.js";
|
|
54
55
|
import { FilterField, FilterFieldProps, Toolbar, ToolbarProps } from "./primitives/toolbar.js";
|
|
55
56
|
import { Logo, LogoProps, TangleKnot } from "@tangle-network/brand";
|
|
56
|
-
export { Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, type AlertProps, AlertTitle, type AlertTone, ArtifactPane, type ArtifactPaneProps, Avatar, AvatarFallback, AvatarImage, Badge, type BadgeProps, Button, type ButtonProps, CATEGORY_TONES, Calendar, type CalendarDate, type CalendarProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardTitleProps, type CategoryTone, Checkbox, Chip, type ChipProps, CodeBlock, type CodeBlockProps, Collapsible, CollapsibleContent, CollapsibleTrigger, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, DatePicker, type DatePickerProps, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropZone, type DropZoneProps, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, type EmptyStateProps, FilterField, type FilterFieldProps, Heading, type HeadingProps, type HeadingVariant, HelpText, type HelpTextProps, IconTile, type IconTileProps, InlineCode, type InlineCodeProps, Input, type InputProps, Label, Logo, type LogoProps, Metric, type MetricProps, MetricStrip, type MetricStripProps, PageHeader, type PageHeaderProps, PageShell, type PageShellProps, Pagination, type PaginationProps, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, RadioGroup, RadioGroupItem, SegmentedControl, type SegmentedControlOption, type SegmentedControlProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, type SidebarDropZoneProps, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, type StatCardProps, StatusPill, type StatusPillProps, type StatusTone, Switch, TONE_CLASSES, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, type TableProps, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, type TagProps, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, type TextShimmerProps, Textarea, type TextareaProps, ThemeToggle, TimeSelect, type TimeSelectProps, type Toast, ToastContainer, ToastProvider, type Tone, type ToneClasses, Toolbar, type ToolbarProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, type UploadFile, UploadProgress, type UploadProgressProps, badgeVariants, buttonVariants, formatCalendarDate, initialsOf, localToday, parseCalendarDate, toneFor, useTheme, useToast };
|
|
57
|
+
export { Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, type AlertProps, AlertTitle, type AlertTone, ArtifactPane, type ArtifactPaneProps, Avatar, AvatarFallback, AvatarImage, Badge, type BadgeProps, Button, type ButtonProps, CATEGORY_TONES, Calendar, type CalendarDate, type CalendarProps, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, type CardTitleProps, type CategoryTone, Checkbox, Chip, type ChipProps, CodeBlock, type CodeBlockProps, Collapsible, CollapsibleContent, CollapsibleTrigger, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, DatePicker, type DatePickerProps, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropZone, type DropZoneProps, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, type EmptyStateProps, FilterField, type FilterFieldProps, Heading, type HeadingProps, type HeadingVariant, HelpText, type HelpTextProps, IconTile, type IconTileProps, InlineCode, type InlineCodeProps, Input, type InputProps, Label, Logo, type LogoProps, Metric, type MetricProps, MetricStrip, type MetricStripProps, PageHeader, type PageHeaderProps, PageShell, type PageShellProps, Pagination, type PaginationProps, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, RadioGroup, RadioGroupItem, SegmentedControl, type SegmentedControlOption, type SegmentedControlProps, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, type SidebarDropZoneProps, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, type StatCardProps, StatusPill, type StatusPillProps, type StatusTone, Switch, TONE_CLASSES, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, type TableProps, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, type TagProps, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, type TextShimmerProps, Textarea, type TextareaProps, ThemeToggle, TimeSelect, type TimeSelectProps, type Toast, ToastContainer, ToastProvider, type Tone, type ToneClasses, Toolbar, type ToolbarProps, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, type UploadFile, UploadProgress, type UploadProgressProps, Wizard, type WizardProps, WizardStep, type WizardStepProps, badgeVariants, buttonVariants, formatCalendarDate, initialsOf, localToday, parseCalendarDate, toneFor, useTheme, useToast };
|
package/dist/primitives.js
CHANGED
|
@@ -49,8 +49,9 @@ import { StatusPill } from "./primitives/status-pill.js";
|
|
|
49
49
|
import { CATEGORY_TONES, TONE_CLASSES, toneFor } from "./primitives/tone.js";
|
|
50
50
|
import { Tag } from "./primitives/tag.js";
|
|
51
51
|
import { Chip } from "./primitives/chip.js";
|
|
52
|
+
import { Wizard, WizardStep } from "./primitives/wizard.js";
|
|
52
53
|
import { IconTile, initialsOf } from "./primitives/icon-tile.js";
|
|
53
54
|
import { Metric, MetricStrip } from "./primitives/metric-strip.js";
|
|
54
55
|
import { FilterField, Toolbar } from "./primitives/toolbar.js";
|
|
55
56
|
import { Logo, TangleKnot } from "@tangle-network/brand";
|
|
56
|
-
export { Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, ArtifactPane, Avatar, AvatarFallback, AvatarImage, Badge, Button, CATEGORY_TONES, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, Chip, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, DatePicker, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropZone, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, FilterField, Heading, HelpText, IconTile, InlineCode, Input, Label, Logo, Metric, MetricStrip, PageHeader, PageShell, Pagination, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, RadioGroup, RadioGroupItem, SegmentedControl, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, StatusPill, Switch, TONE_CLASSES, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, Textarea, ThemeToggle, TimeSelect, ToastContainer, ToastProvider, Toolbar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadProgress, badgeVariants, buttonVariants, formatCalendarDate, initialsOf, localToday, parseCalendarDate, toneFor, useTheme, useToast };
|
|
57
|
+
export { Alert, AlertDescription, AlertDialog, AlertDialogAction, AlertDialogCancel, AlertDialogContent, AlertDialogDescription, AlertDialogFooter, AlertDialogHeader, AlertDialogOverlay, AlertDialogPortal, AlertDialogTitle, AlertDialogTrigger, AlertTitle, ArtifactPane, Avatar, AvatarFallback, AvatarImage, Badge, Button, CATEGORY_TONES, Calendar, Card, CardContent, CardDescription, CardFooter, CardHeader, CardTitle, Checkbox, Chip, CodeBlock, Collapsible, CollapsibleContent, CollapsibleTrigger, ContextMenu, ContextMenuCheckboxItem, ContextMenuContent, ContextMenuGroup, ContextMenuItem, ContextMenuLabel, ContextMenuPortal, ContextMenuRadioGroup, ContextMenuRadioItem, ContextMenuSeparator, ContextMenuShortcut, ContextMenuSub, ContextMenuSubContent, ContextMenuSubTrigger, ContextMenuTrigger, CopyButton, DatePicker, Dialog, DialogClose, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogOverlay, DialogPortal, DialogTitle, DialogTrigger, DropZone, DropdownMenu, DropdownMenuCheckboxItem, DropdownMenuContent, DropdownMenuGroup, DropdownMenuItem, DropdownMenuLabel, DropdownMenuPortal, DropdownMenuRadioGroup, DropdownMenuRadioItem, DropdownMenuSeparator, DropdownMenuShortcut, DropdownMenuSub, DropdownMenuSubContent, DropdownMenuSubTrigger, DropdownMenuTrigger, EmptyState, FilterField, Heading, HelpText, IconTile, InlineCode, Input, Label, Logo, Metric, MetricStrip, PageHeader, PageShell, Pagination, Popover, PopoverAnchor, PopoverClose, PopoverContent, PopoverTrigger, Progress, RadioGroup, RadioGroupItem, SegmentedControl, Select, SelectContent, SelectGroup, SelectItem, SelectLabel, SelectScrollDownButton, SelectScrollUpButton, SelectSeparator, SelectTrigger, SelectValue, Separator, Sheet, SheetClose, SheetContent, SheetDescription, SheetFooter, SheetHeader, SheetOverlay, SheetPortal, SheetTitle, SheetTrigger, SidebarDropZone, Skeleton, SkeletonCard, SkeletonTable, Slider, StatCard, StatusPill, Switch, TONE_CLASSES, Table, TableBody, TableCaption, TableCell, TableFooter, TableHead, TableHeader, TableRow, Tabs, TabsContent, TabsList, TabsTrigger, Tag, TangleKnot, TerminalCursor, TerminalDisplay, TerminalInput, TerminalLine, TextShimmer, Textarea, ThemeToggle, TimeSelect, ToastContainer, ToastProvider, Toolbar, Tooltip, TooltipContent, TooltipProvider, TooltipTrigger, UploadProgress, Wizard, WizardStep, badgeVariants, buttonVariants, formatCalendarDate, initialsOf, localToday, parseCalendarDate, toneFor, useTheme, useToast };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tangle-network/ui",
|
|
3
|
-
"version": "11.
|
|
3
|
+
"version": "11.38.0",
|
|
4
4
|
"description": "Generic React UI components for Tangle products — primitives, chat, run, files, editor, markdown.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -120,6 +120,11 @@
|
|
|
120
120
|
"default": "./dist/presentation.js"
|
|
121
121
|
},
|
|
122
122
|
"./styles/presentation.css": "./src/styles/presentation.css",
|
|
123
|
+
"./code-presentation": {
|
|
124
|
+
"types": "./dist/code-presentation.d.ts",
|
|
125
|
+
"import": "./dist/code-presentation.js",
|
|
126
|
+
"default": "./dist/code-presentation.js"
|
|
127
|
+
},
|
|
123
128
|
"./styles/compat.css": "./src/styles/compat.css"
|
|
124
129
|
},
|
|
125
130
|
"dependencies": {
|
|
@@ -148,7 +153,7 @@
|
|
|
148
153
|
"github-slugger": "^2.0.0",
|
|
149
154
|
"lucide-react": "^1.48.0",
|
|
150
155
|
"marked": "^18.0.14",
|
|
151
|
-
"motion": "14.
|
|
156
|
+
"motion": "14.1.0",
|
|
152
157
|
"react-markdown": "^10.1.0",
|
|
153
158
|
"rehype-sanitize": "^6.0.0",
|
|
154
159
|
"rehype-slug": "^6.0.0",
|
|
@@ -0,0 +1,2 @@
|
|
|
1
|
+
/** Framework-neutral highlighting, copy and tabs; no React runtime dependency. */
|
|
2
|
+
export { codeBlockGrammar, highlightCode, codePreStyle, codeTextStyle, codePresentation, bindCopyButton, bindCodeTabs, createCopyFeedback, interactionBudgets, writeClipboard, apiMetadataText, schemaRows, type CopyFeedback, type InteractionBudgets, type CodeExample, type ApiMetadata, type ApiField, type TokenStyle } from "./lib/code-presentation";
|
|
@@ -0,0 +1,257 @@
|
|
|
1
|
+
import { tabsPresentation } from "../presentation";
|
|
2
|
+
import type { HighlighterCore, LanguageRegistration, ThemedToken } from "shiki/core";
|
|
3
|
+
// Leave variables unresolved: the browser resolves them on each token span,
|
|
4
|
+
// including nested themes, inline overrides and CSS/media changes. No document
|
|
5
|
+
// sampling, observers, layout reads or React re-render are needed to recolor code.
|
|
6
|
+
export const SYNTAX = {
|
|
7
|
+
comment: "var(--syntax-comment, currentColor)",
|
|
8
|
+
keyword: "var(--syntax-keyword, currentColor)",
|
|
9
|
+
string: "var(--syntax-string, currentColor)",
|
|
10
|
+
function: "var(--syntax-function, currentColor)",
|
|
11
|
+
number: "var(--syntax-number, currentColor)",
|
|
12
|
+
variable: "var(--syntax-variable, currentColor)",
|
|
13
|
+
foreground: "var(--syntax-foreground, currentColor)",
|
|
14
|
+
} as const;
|
|
15
|
+
|
|
16
|
+
const SHIKI_THEME = "tangle-syntax";
|
|
17
|
+
// Shiki's CSS-variables theme names token roles; each maps onto a Brand syntax token.
|
|
18
|
+
const BRAND_COLOR_FOR_SHIKI: Record<string, string> = {
|
|
19
|
+
"var(--shiki-foreground)": SYNTAX.foreground,
|
|
20
|
+
"var(--shiki-token-comment)": SYNTAX.comment,
|
|
21
|
+
"var(--shiki-token-keyword)": SYNTAX.keyword,
|
|
22
|
+
"var(--shiki-token-string)": SYNTAX.string,
|
|
23
|
+
"var(--shiki-token-string-expression)": SYNTAX.string,
|
|
24
|
+
"var(--shiki-token-function)": SYNTAX.function,
|
|
25
|
+
"var(--shiki-token-constant)": SYNTAX.number,
|
|
26
|
+
"var(--shiki-token-link)": SYNTAX.number,
|
|
27
|
+
"var(--shiki-token-parameter)": SYNTAX.variable,
|
|
28
|
+
"var(--shiki-token-punctuation)": SYNTAX.foreground,
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
type GrammarModule = { default: LanguageRegistration[] };
|
|
32
|
+
const GRAMMARS: Record<string, () => Promise<GrammarModule>> = {
|
|
33
|
+
typescript: () => import("shiki/langs/typescript.mjs"),
|
|
34
|
+
tsx: () => import("shiki/langs/tsx.mjs"),
|
|
35
|
+
javascript: () => import("shiki/langs/javascript.mjs"),
|
|
36
|
+
jsx: () => import("shiki/langs/jsx.mjs"),
|
|
37
|
+
json: () => import("shiki/langs/json.mjs"),
|
|
38
|
+
jsonc: () => import("shiki/langs/jsonc.mjs"),
|
|
39
|
+
bash: () => import("shiki/langs/bash.mjs"),
|
|
40
|
+
python: () => import("shiki/langs/python.mjs"),
|
|
41
|
+
html: () => import("shiki/langs/html.mjs"),
|
|
42
|
+
css: () => import("shiki/langs/css.mjs"),
|
|
43
|
+
scss: () => import("shiki/langs/scss.mjs"),
|
|
44
|
+
markdown: () => import("shiki/langs/markdown.mjs"),
|
|
45
|
+
mdx: () => import("shiki/langs/mdx.mjs"),
|
|
46
|
+
yaml: () => import("shiki/langs/yaml.mjs"),
|
|
47
|
+
toml: () => import("shiki/langs/toml.mjs"),
|
|
48
|
+
sql: () => import("shiki/langs/sql.mjs"),
|
|
49
|
+
diff: () => import("shiki/langs/diff.mjs"),
|
|
50
|
+
go: () => import("shiki/langs/go.mjs"),
|
|
51
|
+
rust: () => import("shiki/langs/rust.mjs"),
|
|
52
|
+
solidity: () => import("shiki/langs/solidity.mjs"),
|
|
53
|
+
ruby: () => import("shiki/langs/ruby.mjs"),
|
|
54
|
+
java: () => import("shiki/langs/java.mjs"),
|
|
55
|
+
kotlin: () => import("shiki/langs/kotlin.mjs"),
|
|
56
|
+
swift: () => import("shiki/langs/swift.mjs"),
|
|
57
|
+
php: () => import("shiki/langs/php.mjs"),
|
|
58
|
+
c: () => import("shiki/langs/c.mjs"),
|
|
59
|
+
cpp: () => import("shiki/langs/cpp.mjs"),
|
|
60
|
+
csharp: () => import("shiki/langs/csharp.mjs"),
|
|
61
|
+
graphql: () => import("shiki/langs/graphql.mjs"),
|
|
62
|
+
xml: () => import("shiki/langs/xml.mjs"),
|
|
63
|
+
dockerfile: () => import("shiki/langs/dockerfile.mjs"),
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const LANGUAGE_ALIASES: Record<string, string> = {
|
|
67
|
+
ts: "typescript", mts: "typescript", cts: "typescript", js: "javascript", mjs: "javascript",
|
|
68
|
+
cjs: "javascript", node: "javascript", sh: "bash", shell: "bash", zsh: "bash", console: "bash",
|
|
69
|
+
shellscript: "bash", py: "python", python3: "python", yml: "yaml", md: "markdown", rs: "rust",
|
|
70
|
+
sol: "solidity", rb: "ruby", kt: "kotlin", cs: "csharp", "c#": "csharp", "c++": "cpp",
|
|
71
|
+
gql: "graphql", svg: "xml", docker: "dockerfile", patch: "diff", htm: "html",
|
|
72
|
+
};
|
|
73
|
+
|
|
74
|
+
export const LANGUAGE_NAMES: Record<string, string> = {
|
|
75
|
+
typescript: "TypeScript", tsx: "TSX", javascript: "JavaScript", jsx: "JSX", json: "JSON",
|
|
76
|
+
jsonc: "JSONC", bash: "Bash", python: "Python", html: "HTML", css: "CSS", scss: "SCSS",
|
|
77
|
+
markdown: "Markdown", mdx: "MDX", yaml: "YAML", toml: "TOML", sql: "SQL", diff: "Diff",
|
|
78
|
+
go: "Go", rust: "Rust", solidity: "Solidity", ruby: "Ruby", java: "Java", kotlin: "Kotlin",
|
|
79
|
+
swift: "Swift", php: "PHP", c: "C", cpp: "C++", csharp: "C#", graphql: "GraphQL", xml: "XML",
|
|
80
|
+
dockerfile: "Dockerfile",
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
/** The Shiki grammar for a fence language, or undefined for plain text. */
|
|
84
|
+
export const PLAIN_TEXT = new Set(["text", "txt", "plain", "plaintext"]);
|
|
85
|
+
|
|
86
|
+
export function codeBlockGrammar(language: string | undefined): string | undefined {
|
|
87
|
+
if (!language) return undefined;
|
|
88
|
+
const id = language.toLowerCase();
|
|
89
|
+
const resolved = LANGUAGE_ALIASES[id] ?? id;
|
|
90
|
+
return GRAMMARS[resolved] ? resolved : undefined;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
let highlighterPromise: Promise<HighlighterCore> | undefined;
|
|
94
|
+
const grammarLoads = new Map<string, Promise<void>>();
|
|
95
|
+
|
|
96
|
+
function loadHighlighter(): Promise<HighlighterCore> {
|
|
97
|
+
// The JavaScript regex engine needs no WebAssembly, so it runs under a strict CSP.
|
|
98
|
+
return highlighterPromise ??= Promise.all([import("shiki/core"), import("shiki/engine/javascript")])
|
|
99
|
+
.then(([core, engine]) => core.createHighlighterCore({
|
|
100
|
+
themes: [core.createCssVariablesTheme({ name: SHIKI_THEME, variablePrefix: "--shiki-", fontStyle: true })],
|
|
101
|
+
langs: [],
|
|
102
|
+
engine: engine.createJavaScriptRegexEngine(),
|
|
103
|
+
}))
|
|
104
|
+
.catch((error: unknown) => {
|
|
105
|
+
highlighterPromise = undefined;
|
|
106
|
+
throw error;
|
|
107
|
+
});
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export async function tokenize(code: string, grammar: string): Promise<ThemedToken[][]> {
|
|
111
|
+
const highlighter = await loadHighlighter();
|
|
112
|
+
let load = grammarLoads.get(grammar);
|
|
113
|
+
if (!load) {
|
|
114
|
+
load = GRAMMARS[grammar]!().then((module) => highlighter.loadLanguage(module.default));
|
|
115
|
+
load.catch(() => grammarLoads.delete(grammar));
|
|
116
|
+
grammarLoads.set(grammar, load);
|
|
117
|
+
}
|
|
118
|
+
await load;
|
|
119
|
+
return highlighter.codeToTokensBase(code, { lang: grammar, theme: SHIKI_THEME });
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export function tokenStyle(token: ThemedToken): TokenStyle | undefined {
|
|
123
|
+
const color = token.color ? BRAND_COLOR_FOR_SHIKI[token.color] ?? SYNTAX.foreground : undefined;
|
|
124
|
+
const style = token.fontStyle ?? 0;
|
|
125
|
+
if (!color && !style) return undefined;
|
|
126
|
+
return {
|
|
127
|
+
...(color ? { color } : {}),
|
|
128
|
+
...(style & 1 ? { fontStyle: "italic" } : {}),
|
|
129
|
+
...(style & 2 ? { fontWeight: "bold" } : {}),
|
|
130
|
+
...(style & 4 ? { textDecoration: "underline" } : {}),
|
|
131
|
+
};
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
export interface TokenStyle {color?: string; fontStyle?: "italic"; fontWeight?: "bold"; textDecoration?: "underline";}
|
|
135
|
+
export const codePreStyle = {
|
|
136
|
+
margin: 0, padding: "var(--code-padding-y, 0.625rem) var(--code-padding-x, 0.75rem)",
|
|
137
|
+
background: "transparent", color: SYNTAX.foreground, fontSize: "var(--code-font-size, 0.8125rem)",
|
|
138
|
+
lineHeight: "var(--code-line-height, 1.5)", overflowX: "auto",
|
|
139
|
+
} as const;
|
|
140
|
+
export const codeTextStyle = {fontFamily: "var(--font-mono, 'JetBrains Mono', ui-monospace, monospace)",whiteSpace:"pre"} as const;
|
|
141
|
+
export const codePresentation = {
|
|
142
|
+
tabs: `tangle-code-tabs ${tabsPresentation.list.underline}`,
|
|
143
|
+
tab: tabsPresentation.trigger.underline,
|
|
144
|
+
root: "group relative min-w-0 overflow-hidden rounded-lg border font-mono bg-card border-border",
|
|
145
|
+
header: "flex flex-wrap items-center justify-between gap-3 border-b border-border px-3 py-1.5 bg-[var(--code-header-bg,hsl(var(--background)))]",
|
|
146
|
+
copy: "flex items-center justify-center min-w-8 min-h-8 pointer-coarse:min-w-11 pointer-coarse:min-h-11 rounded-md bg-muted border border-border hover:border-[var(--border-strong)] transition-colors data-[copy-loading=true]:after:content-['…'] data-[copy-loading=true]:gap-1",
|
|
147
|
+
copyStatus: "sr-only data-[copy-long-work=true]:not-sr-only data-[copy-long-work=true]:text-xs data-[copy-long-work=true]:text-muted-foreground",
|
|
148
|
+
};
|
|
149
|
+
export interface CodeExample {label:string;code:string;language?:string;filename?:string;}
|
|
150
|
+
export interface ApiMetadata {kind:"request"|"response"|"error";method?:string;status?:number;contentType?:string;}
|
|
151
|
+
export const apiMetadataText = (meta:ApiMetadata) => [meta.kind,meta.method,meta.status,meta.contentType].filter(v=>v!==undefined).join(" · ");
|
|
152
|
+
|
|
153
|
+
/** Same lazy grammar/token model for React, Hono SSR and static renderers. */
|
|
154
|
+
export async function highlightCode(code:string,language?:string) {
|
|
155
|
+
const grammar=codeBlockGrammar(language);
|
|
156
|
+
if(!grammar)return code.split("\n").map(content=>[{content,style:undefined as TokenStyle|undefined}]);
|
|
157
|
+
return (await tokenize(code,grammar)).map(line=>line.map(token=>({content:token.content,style:tokenStyle(token)})));
|
|
158
|
+
}
|
|
159
|
+
/** Legacy fallback is used only when the modern clipboard API is absent. */
|
|
160
|
+
export async function writeClipboard(text:string):Promise<void> {
|
|
161
|
+
if(navigator.clipboard?.writeText){await navigator.clipboard.writeText(text);return;}
|
|
162
|
+
const active=document.activeElement as HTMLElement|null,selection=document.getSelection();
|
|
163
|
+
const ranges=selection?Array.from({length:selection.rangeCount},(_,i)=>selection.getRangeAt(i).cloneRange()):[];
|
|
164
|
+
const field=document.createElement("textarea");field.value=text;field.readOnly=true;field.tabIndex=-1;
|
|
165
|
+
field.setAttribute("aria-hidden","true");field.style.position="fixed";field.style.left="-9999px";
|
|
166
|
+
document.body.append(field);
|
|
167
|
+
try {field.select();if(!document.execCommand?.("copy"))throw Error("Clipboard unavailable");}
|
|
168
|
+
finally {field.remove();active?.focus({preventScroll:true});if(selection){selection.removeAllRanges();for(const range of ranges)selection.addRange(range);}}
|
|
169
|
+
}
|
|
170
|
+
const COPY_FEEDBACK_MS=2000;
|
|
171
|
+
export interface InteractionBudgets { feedback?:number; pending?:number; longWork?:number; mutation?:number; }
|
|
172
|
+
/** Resolve scoped Brand times, including calc(), without duplicating token values. */
|
|
173
|
+
export function interactionBudgets(scope:HTMLElement):InteractionBudgets {
|
|
174
|
+
const view=scope.ownerDocument.defaultView;if(!view)return {};
|
|
175
|
+
const probe=scope.ownerDocument.createElement("i");probe.hidden=true;scope.append(probe);
|
|
176
|
+
const read=(role:string)=>{
|
|
177
|
+
const name=`--interaction-${role}`;
|
|
178
|
+
if(!view.getComputedStyle(scope).getPropertyValue(name).trim())return undefined;
|
|
179
|
+
probe.style.animationDuration=`var(${name})`;
|
|
180
|
+
const time=/^(\d+(?:\.\d+)?)(ms|s)$/.exec(view.getComputedStyle(probe).animationDuration);
|
|
181
|
+
return time?Number(time[1])*(time[2]==="s"?1000:1):undefined;
|
|
182
|
+
};
|
|
183
|
+
try{return {feedback:read("feedback-budget"),pending:read("pending-delay"),longWork:read("long-work-delay"),mutation:read("mutation-budget")};}
|
|
184
|
+
finally{probe.remove();}
|
|
185
|
+
}
|
|
186
|
+
export interface CopyFeedback {
|
|
187
|
+
copied:boolean;status:string;pending:boolean;loading?:boolean;longRunning?:boolean;elapsedMs?:number;
|
|
188
|
+
measurement?:{elapsedMs:number;feedbackMs:number;budgets:InteractionBudgets;mutationBudgetExceeded:boolean|undefined};
|
|
189
|
+
}
|
|
190
|
+
/** Owns one accepted write, scoped delays and cleanup for both renderers. */
|
|
191
|
+
export function createCopyFeedback(change:(state:CopyFeedback)=>void,write:(text:string)=>Promise<void>=writeClipboard,scope?:HTMLElement) {
|
|
192
|
+
let disposed=false,pending=false,timer:ReturnType<typeof setTimeout>|undefined;
|
|
193
|
+
let delays:Array<ReturnType<typeof setTimeout>>=[],elapsed:ReturnType<typeof setInterval>|undefined;
|
|
194
|
+
const clearPending=()=>{for(const delay of delays)clearTimeout(delay);delays=[];if(elapsed)clearInterval(elapsed);elapsed=undefined;};
|
|
195
|
+
return {
|
|
196
|
+
async copy(text:string) {
|
|
197
|
+
if(disposed||pending)return;
|
|
198
|
+
const started=performance.now(),budgets=scope?interactionBudgets(scope):{};
|
|
199
|
+
pending=true;if(timer)clearTimeout(timer);
|
|
200
|
+
let loading=budgets.pending===undefined,longRunning=false;
|
|
201
|
+
const announce=()=>{
|
|
202
|
+
const elapsedMs=performance.now()-started;
|
|
203
|
+
change({copied:false,pending:true,loading,longRunning,elapsedMs,status:longRunning
|
|
204
|
+
?`Copying · ${Math.floor(elapsedMs/1000)}s elapsed. Time remaining unknown. Cannot cancel this clipboard write; waiting for confirmation.`
|
|
205
|
+
:"Copying to clipboard"});
|
|
206
|
+
};
|
|
207
|
+
announce();const feedbackMs=performance.now()-started;
|
|
208
|
+
if(budgets.pending!==undefined)delays.push(setTimeout(()=>{loading=true;announce();},budgets.pending));
|
|
209
|
+
if(budgets.longWork!==undefined)delays.push(setTimeout(()=>{longRunning=true;announce();elapsed=setInterval(announce,1000);},budgets.longWork));
|
|
210
|
+
let copied:boolean;
|
|
211
|
+
try {await write(text);copied=true;}
|
|
212
|
+
catch {copied=false;}
|
|
213
|
+
pending=false;clearPending();if(disposed)return;
|
|
214
|
+
const elapsedMs=performance.now()-started;
|
|
215
|
+
change({copied,status:copied?"Copied to clipboard":"Copy failed",pending:false,measurement:{elapsedMs,feedbackMs,budgets,
|
|
216
|
+
mutationBudgetExceeded:budgets.mutation===undefined?undefined:elapsedMs>budgets.mutation}});
|
|
217
|
+
timer=setTimeout(()=>{if(!disposed)change({copied:false,status:"",pending:false});},COPY_FEEDBACK_MS);
|
|
218
|
+
},
|
|
219
|
+
dispose(){disposed=true;clearPending();if(timer)clearTimeout(timer);},
|
|
220
|
+
};
|
|
221
|
+
}
|
|
222
|
+
export function bindCopyButton(button:HTMLButtonElement,status:HTMLElement,text:()=>string) {
|
|
223
|
+
const previous={label:button.getAttribute("aria-label"),status:status.textContent,disabled:button.getAttribute("aria-disabled"),busy:button.getAttribute("aria-busy"),loading:button.getAttribute("data-copy-loading"),long:status.getAttribute("data-copy-long-work")};
|
|
224
|
+
const restore=(element:HTMLElement,name:string,value:string|null)=>{if(value===null)element.removeAttribute(name);else element.setAttribute(name,value);};
|
|
225
|
+
const feedback=createCopyFeedback(state=>{
|
|
226
|
+
status.textContent=state.status;status.setAttribute("data-copy-long-work",String(!!state.longRunning));
|
|
227
|
+
button.setAttribute("data-copy-loading",String(!!state.loading));
|
|
228
|
+
button.setAttribute("aria-label",state.pending?"Copying to clipboard":state.copied?"Copied":"Copy to clipboard");
|
|
229
|
+
if(state.pending){button.setAttribute("aria-disabled","true");button.setAttribute("aria-busy","true");}
|
|
230
|
+
else {restore(button,"aria-disabled",previous.disabled);restore(button,"aria-busy",previous.busy);}
|
|
231
|
+
},writeClipboard,button);
|
|
232
|
+
const click=()=>{if(!button.disabled&&button.getAttribute("aria-disabled")!=="true")void feedback.copy(text());};button.addEventListener("click",click);
|
|
233
|
+
return ()=>{button.removeEventListener("click",click);feedback.dispose();restore(button,"aria-label",previous.label);status.textContent=previous.status;restore(button,"aria-disabled",previous.disabled);restore(button,"aria-busy",previous.busy);restore(button,"data-copy-loading",previous.loading);restore(status,"data-copy-long-work",previous.long);};
|
|
234
|
+
}
|
|
235
|
+
/** Native tabs adapter. The consumer owns labels, IDs and escaped panel content. */
|
|
236
|
+
export function bindCodeTabs(list:HTMLElement,onSelect?:(index:number)=>void) {
|
|
237
|
+
const buttons=()=>[...list.querySelectorAll<HTMLButtonElement>('[role="tab"]')];
|
|
238
|
+
const select=(index:number,focus:boolean)=>{
|
|
239
|
+
const tabs=buttons(),tab=tabs[index];if(!tab)return;
|
|
240
|
+
for(const panel of new Set(tabs.map(t=>document.getElementById(t.getAttribute("aria-controls")??""))))if(panel)panel.hidden=true;
|
|
241
|
+
tabs.forEach((t,i)=>{t.setAttribute("aria-selected",String(i===index));t.dataset.state=i===index?"active":"inactive";t.tabIndex=i===index?0:-1;});
|
|
242
|
+
const panel=document.getElementById(tab.getAttribute("aria-controls")??"");if(panel){panel.hidden=false;panel.setAttribute("aria-labelledby",tab.id);}
|
|
243
|
+
if(focus)tab.focus();onSelect?.(index);
|
|
244
|
+
};
|
|
245
|
+
const click=(event:MouseEvent)=>{const tab=(event.target as Element).closest('[role="tab"]');const index=buttons().findIndex(t=>t===tab);if(index>=0)select(index,false);};
|
|
246
|
+
const key=(event:KeyboardEvent)=>{
|
|
247
|
+
const tabs=buttons(),current=tabs.findIndex(t=>t===document.activeElement);if(current<0)return;
|
|
248
|
+
const next=event.key==="Home"?0:event.key==="End"?tabs.length-1:event.key==="ArrowRight"?(current+1)%tabs.length:event.key==="ArrowLeft"?(current+tabs.length-1)%tabs.length:undefined;
|
|
249
|
+
if(next!==undefined){event.preventDefault();select(next,true);}
|
|
250
|
+
};
|
|
251
|
+
list.addEventListener("click",click);list.addEventListener("keydown",key);
|
|
252
|
+
return ()=>{list.removeEventListener("click",click);list.removeEventListener("keydown",key);};
|
|
253
|
+
}
|
|
254
|
+
export interface ApiField {name:string;type:string;description?:string;required?:boolean;children?:readonly ApiField[];}
|
|
255
|
+
export function schemaRows(fields:readonly ApiField[],parent="",depth=0):Array<ApiField&{path:string;depth:number}> {
|
|
256
|
+
return fields.flatMap(field=>{const path=parent?`${parent}.${field.name}`:field.name;return [{...field,path,depth},...schemaRows(field.children??[],path,depth+1)];});
|
|
257
|
+
}
|
|
@@ -2,11 +2,11 @@
|
|
|
2
2
|
|
|
3
3
|
import * as React from "react";
|
|
4
4
|
import { Slot } from "@radix-ui/react-slot";
|
|
5
|
-
import { animate } from "motion";
|
|
5
|
+
import { animate, motionValue, styleEffect, type MotionValue } from "motion";
|
|
6
6
|
|
|
7
7
|
type Side = "top" | "right" | "bottom" | "left";
|
|
8
8
|
type SurfaceProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
9
|
-
kind: "dialog" | "sheet" | "overlay" | "tooltip";
|
|
9
|
+
kind: "dialog" | "sheet" | "overlay" | "tooltip" | "popover";
|
|
10
10
|
side?: Side;
|
|
11
11
|
asChild?: boolean;
|
|
12
12
|
"data-state"?: string;
|
|
@@ -16,8 +16,8 @@ type SurfaceProps = React.HTMLAttributes<HTMLDivElement> & {
|
|
|
16
16
|
export const SurfaceMotion = React.forwardRef<HTMLDivElement, SurfaceProps>(
|
|
17
17
|
({ kind, side = "right", asChild, "data-state": state, ...props }, forwardedRef) => {
|
|
18
18
|
const element = React.useRef<HTMLDivElement | null>(null);
|
|
19
|
-
const
|
|
20
|
-
const controls = React.useRef<ReturnType<typeof animate
|
|
19
|
+
const values = React.useRef<Record<"opacity" | "scale" | "x" | "y", MotionValue<number>> | null>(null);
|
|
20
|
+
const controls = React.useRef<Array<ReturnType<typeof animate>>>([]);
|
|
21
21
|
const ref = React.useCallback<React.RefCallback<HTMLDivElement>>((node) => {
|
|
22
22
|
element.current = node;
|
|
23
23
|
if (typeof forwardedRef === "function") {
|
|
@@ -30,12 +30,13 @@ export const SurfaceMotion = React.forwardRef<HTMLDivElement, SurfaceProps>(
|
|
|
30
30
|
const node = element.current;
|
|
31
31
|
if (!node) return;
|
|
32
32
|
const preference = typeof matchMedia === "function" ? matchMedia("(prefers-reduced-motion: reduce)") : undefined;
|
|
33
|
+
let cancelRender: (() => void) | undefined;
|
|
33
34
|
const run = () => {
|
|
34
35
|
const closed = state === "closed";
|
|
35
36
|
const bounds = node.getBoundingClientRect();
|
|
36
37
|
const hidden = {
|
|
37
38
|
opacity: kind === "sheet" ? 1 : 0,
|
|
38
|
-
scale: kind === "dialog" || kind === "tooltip" ? 0.98 : 1,
|
|
39
|
+
scale: kind === "dialog" || kind === "tooltip" || kind === "popover" ? 0.98 : 1,
|
|
39
40
|
x: kind === "sheet" ? side === "left" ? -bounds.width : side === "right" ? bounds.width : 0 : 0,
|
|
40
41
|
y: kind === "sheet" ? side === "top" ? -bounds.height : side === "bottom" ? bounds.height : 0 : 0,
|
|
41
42
|
};
|
|
@@ -51,26 +52,32 @@ export const SurfaceMotion = React.forwardRef<HTMLDivElement, SurfaceProps>(
|
|
|
51
52
|
// Missing styles and reduced motion settle synchronously. No semantic
|
|
52
53
|
// transition waits for a JavaScript completion callback.
|
|
53
54
|
if (preference?.matches || seconds <= 0 || style.animationName === "none") {
|
|
54
|
-
controls.current
|
|
55
|
+
controls.current.forEach(control => control.stop());
|
|
56
|
+
if (values.current) for (const key of ["opacity", "scale", "x", "y"] as const) values.current[key].jump(target[key]);
|
|
55
57
|
place(target);
|
|
56
|
-
if (initialized.current) controls.current = animate(node, target, { duration: 0 });
|
|
57
58
|
return;
|
|
58
59
|
}
|
|
59
|
-
if (!
|
|
60
|
+
if (!values.current) {
|
|
61
|
+
place(hidden);
|
|
62
|
+
values.current = { opacity: motionValue(hidden.opacity), scale: motionValue(hidden.scale), x: motionValue(hidden.x), y: motionValue(hidden.y) };
|
|
63
|
+
}
|
|
64
|
+
cancelRender ??= styleEffect(node, values.current);
|
|
60
65
|
// Critically damped response: <0.01% residual at the role's deadline.
|
|
61
|
-
//
|
|
62
|
-
//
|
|
66
|
+
// Explicit values keep every channel on the same frame clock, including
|
|
67
|
+
// opacity, without resampling a compositor animation on reversal.
|
|
63
68
|
const frequency = 12 / seconds;
|
|
64
|
-
controls.current = animate(
|
|
69
|
+
controls.current = (["opacity", "scale", "x", "y"] as const).map(key => animate(values.current![key], target[key], {
|
|
65
70
|
type: "spring", stiffness: frequency * frequency, damping: 2 * frequency,
|
|
66
71
|
restDelta: 0.001, restSpeed: 0.01,
|
|
67
|
-
});
|
|
72
|
+
}));
|
|
68
73
|
};
|
|
69
74
|
run();
|
|
70
75
|
preference?.addEventListener("change", run);
|
|
71
|
-
return () => preference?.removeEventListener("change", run);
|
|
72
|
-
|
|
73
|
-
|
|
76
|
+
return () => { preference?.removeEventListener("change", run); cancelRender?.(); };
|
|
77
|
+
// Radix Popper temporarily suppresses animation until placement resolves.
|
|
78
|
+
// Start the same retained values when that inline override clears.
|
|
79
|
+
}, [state, kind, side, props.style?.animation]);
|
|
80
|
+
React.useEffect(() => () => controls.current.forEach(control => control.stop()), []);
|
|
74
81
|
const Component = asChild ? Slot : "div";
|
|
75
82
|
return <Component {...props} data-state={state} ref={ref} />;
|
|
76
83
|
},
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { fireEvent,render,screen } from "@testing-library/react";
|
|
2
|
+
import { describe,it,expect,vi } from "vitest";
|
|
3
|
+
import { CodeBlock } from "./code-block";
|
|
4
|
+
import { ApiSchema } from "./api-schema";
|
|
5
|
+
import { bindCopyButton,createCopyFeedback,highlightCode } from "../code-presentation";
|
|
6
|
+
describe("shared API presentation",()=>{
|
|
7
|
+
it("renders field paths without turning schema text into HTML",()=>{
|
|
8
|
+
render(<ApiSchema fields={[{name:"messages",type:"array",children:[{name:"content",type:"string",description:"<script>bad()</script>"}]}]}/>);
|
|
9
|
+
expect(screen.getByText("messages.content")).toBeInTheDocument();expect(document.querySelector("script")).toBeNull();
|
|
10
|
+
});
|
|
11
|
+
it("keeps stale clipboard completions and disposed feedback silent",async()=>{
|
|
12
|
+
const updates=vi.fn();let complete!:()=>void;const feedback=createCopyFeedback(updates,()=>new Promise<void>(resolve=>{complete=resolve;}));
|
|
13
|
+
const pending=feedback.copy("bytes");feedback.dispose();complete();await pending;expect(updates).toHaveBeenCalledTimes(1);
|
|
14
|
+
});
|
|
15
|
+
it("blocks duplicate writes until the accepted operation settles",async()=>{
|
|
16
|
+
const updates=vi.fn();let complete!:()=>void;
|
|
17
|
+
const write=vi.fn(()=>new Promise<void>(resolve=>{complete=resolve;}));
|
|
18
|
+
const feedback=createCopyFeedback(updates,write);
|
|
19
|
+
const first=feedback.copy("first");await feedback.copy("duplicate");
|
|
20
|
+
expect(write.mock.calls).toEqual([["first"]]);
|
|
21
|
+
expect(updates).toHaveBeenLastCalledWith(expect.objectContaining({copied:false,status:"Copying to clipboard",pending:true}));
|
|
22
|
+
complete();await first;
|
|
23
|
+
expect(updates).toHaveBeenLastCalledWith(expect.objectContaining({copied:true,status:"Copied to clipboard",pending:false}));
|
|
24
|
+
const next=feedback.copy("next");expect(write).toHaveBeenCalledTimes(2);complete();await next;
|
|
25
|
+
feedback.dispose();await feedback.copy("disposed");expect(write).toHaveBeenCalledTimes(2);
|
|
26
|
+
});
|
|
27
|
+
it("retains exact newlines and plain binary/SSE text",async()=>{
|
|
28
|
+
const text='data: {"delta":"<x>"}\n\ndata: [DONE]\n';const lines=await highlightCode(text,"text");expect(lines.map(line=>line.map(t=>t.content).join("")).join("\n")).toBe(text);
|
|
29
|
+
});
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
describe("copy binding cleanup",()=>{
|
|
33
|
+
it.each(["pending","success","failure"])("restores retained DOM after %s",async phase=>{
|
|
34
|
+
const descriptor=Object.getOwnPropertyDescriptor(navigator,"clipboard");
|
|
35
|
+
let resolve!:()=>void,reject!:(error:Error)=>void;
|
|
36
|
+
const writeText=vi.fn(()=>new Promise<void>((yes,no)=>{resolve=yes;reject=no;}));
|
|
37
|
+
Object.defineProperty(navigator,"clipboard",{configurable:true,value:{writeText}});
|
|
38
|
+
const button=document.createElement("button"),status=document.createElement("span");
|
|
39
|
+
button.setAttribute("aria-label","Copy request");status.textContent="Ready";
|
|
40
|
+
const cleanup=bindCopyButton(button,status,()=>"exact bytes");
|
|
41
|
+
try {
|
|
42
|
+
button.click();expect(button.getAttribute("aria-label")).toBe("Copying to clipboard");
|
|
43
|
+
if(phase!=="pending"){
|
|
44
|
+
if(phase==="failure")reject(new Error("Denied"));else resolve();
|
|
45
|
+
await vi.waitFor(()=>expect(button.getAttribute("aria-label")).toBe(phase==="failure"?"Copy to clipboard":"Copied"));
|
|
46
|
+
}
|
|
47
|
+
cleanup();
|
|
48
|
+
expect(button.getAttribute("aria-label")).toBe("Copy request");expect(status.textContent).toBe("Ready");
|
|
49
|
+
resolve();await Promise.resolve();await Promise.resolve();
|
|
50
|
+
expect(button.getAttribute("aria-label")).toBe("Copy request");expect(status.textContent).toBe("Ready");
|
|
51
|
+
}finally{
|
|
52
|
+
cleanup();if(descriptor)Object.defineProperty(navigator,"clipboard",descriptor);else delete (navigator as {clipboard?:unknown}).clipboard;
|
|
53
|
+
}
|
|
54
|
+
});
|
|
55
|
+
it("restores an originally absent accessible label",()=>{
|
|
56
|
+
const button=document.createElement("button"),status=document.createElement("span");
|
|
57
|
+
const cleanup=bindCopyButton(button,status,()=>"bytes");button.setAttribute("aria-label","Copied");cleanup();expect(button.hasAttribute("aria-label")).toBe(false);
|
|
58
|
+
});
|
|
59
|
+
it("gives a single code example no orphaned tab relationship",()=>{
|
|
60
|
+
const {container,unmount}=render(<CodeBlock code="fallback" examples={[{label:"Plain",language:"text",code:"one"}]}/>);
|
|
61
|
+
try {expect(container.querySelector('[role="tab"]')).toBeNull();expect(container.querySelector('[role="tabpanel"]')).toBeNull();expect(container.querySelector('[aria-labelledby]')).toBeNull();expect(container.querySelector("pre")?.textContent).toBe("one");}finally{unmount();}
|
|
62
|
+
});
|
|
63
|
+
});
|