@saena-io/create 0.1.0 → 0.2.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/index.js +9 -9
- package/package.json +1 -1
- package/template/base/package.json +44 -2
- package/template/base/scripts/ui-update.ts +83 -0
- package/template/base/src/components/ui/accordion.tsx +75 -0
- package/template/base/src/components/ui/alert-dialog.tsx +162 -0
- package/template/base/src/components/ui/alert.tsx +73 -0
- package/template/base/src/components/ui/app-sidebar.tsx +183 -0
- package/template/base/src/components/ui/aspect-ratio.tsx +22 -0
- package/template/base/src/components/ui/asset-input.tsx +211 -0
- package/template/base/src/components/ui/avatar.tsx +91 -0
- package/template/base/src/components/ui/badge.tsx +50 -0
- package/template/base/src/components/ui/breadcrumb.tsx +104 -0
- package/template/base/src/components/ui/button-group.tsx +78 -0
- package/template/base/src/components/ui/button.tsx +56 -0
- package/template/base/src/components/ui/calendar.tsx +205 -0
- package/template/base/src/components/ui/card.tsx +85 -0
- package/template/base/src/components/ui/carousel.tsx +232 -0
- package/template/base/src/components/ui/chart.tsx +337 -0
- package/template/base/src/components/ui/checkbox.tsx +29 -0
- package/template/base/src/components/ui/collapsible.tsx +15 -0
- package/template/base/src/components/ui/combobox.tsx +276 -0
- package/template/base/src/components/ui/command.tsx +190 -0
- package/template/base/src/components/ui/context-menu.tsx +243 -0
- package/template/base/src/components/ui/dialog.tsx +134 -0
- package/template/base/src/components/ui/direction.tsx +4 -0
- package/template/base/src/components/ui/drawer.tsx +120 -0
- package/template/base/src/components/ui/dropdown-menu.tsx +254 -0
- package/template/base/src/components/ui/empty.tsx +94 -0
- package/template/base/src/components/ui/field.tsx +222 -0
- package/template/base/src/components/ui/focal-point-picker.tsx +175 -0
- package/template/base/src/components/ui/hover-card.tsx +46 -0
- package/template/base/src/components/ui/input-group.tsx +149 -0
- package/template/base/src/components/ui/input-otp.tsx +85 -0
- package/template/base/src/components/ui/input.tsx +20 -0
- package/template/base/src/components/ui/item.tsx +188 -0
- package/template/base/src/components/ui/kbd.tsx +26 -0
- package/template/base/src/components/ui/label.tsx +20 -0
- package/template/base/src/components/ui/menubar.tsx +268 -0
- package/template/base/src/components/ui/native-select.tsx +58 -0
- package/template/base/src/components/ui/nav-main.tsx +70 -0
- package/template/base/src/components/ui/nav-projects.tsx +97 -0
- package/template/base/src/components/ui/nav-secondary.tsx +37 -0
- package/template/base/src/components/ui/nav-user.tsx +108 -0
- package/template/base/src/components/ui/navigation-menu.tsx +164 -0
- package/template/base/src/components/ui/pagination.tsx +123 -0
- package/template/base/src/components/ui/popover.tsx +80 -0
- package/template/base/src/components/ui/progress.tsx +66 -0
- package/template/base/src/components/ui/radio-group.tsx +36 -0
- package/template/base/src/components/ui/resizable.tsx +42 -0
- package/template/base/src/components/ui/rich-text/ai-chat-editor.tsx +20 -0
- package/template/base/src/components/ui/rich-text/ai-command.tsx +90 -0
- package/template/base/src/components/ui/rich-text/ai-copilot.tsx +67 -0
- package/template/base/src/components/ui/rich-text/ai-menu.tsx +456 -0
- package/template/base/src/components/ui/rich-text/ai-node.tsx +42 -0
- package/template/base/src/components/ui/rich-text/ai-toolbar-button.tsx +29 -0
- package/template/base/src/components/ui/rich-text/block-draggable.tsx +187 -0
- package/template/base/src/components/ui/rich-text/block-selection.tsx +17 -0
- package/template/base/src/components/ui/rich-text/code-block-node.tsx +204 -0
- package/template/base/src/components/ui/rich-text/codec.ts +63 -0
- package/template/base/src/components/ui/rich-text/extension.ts +53 -0
- package/template/base/src/components/ui/rich-text/ghost-text.tsx +23 -0
- package/template/base/src/components/ui/rich-text/import-export-toolbar.tsx +103 -0
- package/template/base/src/components/ui/rich-text/link.tsx +18 -0
- package/template/base/src/components/ui/rich-text/list-node.tsx +65 -0
- package/template/base/src/components/ui/rich-text/nodes.tsx +44 -0
- package/template/base/src/components/ui/rich-text/plugins.ts +233 -0
- package/template/base/src/components/ui/rich-text/rich-text-editor.tsx +82 -0
- package/template/base/src/components/ui/rich-text/static.tsx +117 -0
- package/template/base/src/components/ui/rich-text/table-node.tsx +934 -0
- package/template/base/src/components/ui/rich-text/table-toolbar.tsx +232 -0
- package/template/base/src/components/ui/rich-text/toggle-node.tsx +36 -0
- package/template/base/src/components/ui/rich-text/toolbar-slots.ts +41 -0
- package/template/base/src/components/ui/rich-text/toolbar.tsx +668 -0
- package/template/base/src/components/ui/rich-text/use-ai-chat.ts +35 -0
- package/template/base/src/components/ui/rich-text/variable-type.ts +4 -0
- package/template/base/src/components/ui/rich-text/variable.tsx +97 -0
- package/template/base/src/components/ui/scroll-area.tsx +49 -0
- package/template/base/src/components/ui/select.tsx +202 -0
- package/template/base/src/components/ui/separator.tsx +19 -0
- package/template/base/src/components/ui/sheet.tsx +126 -0
- package/template/base/src/components/ui/sidebar.tsx +695 -0
- package/template/base/src/components/ui/skeleton.tsx +13 -0
- package/template/base/src/components/ui/slider.tsx +52 -0
- package/template/base/src/components/ui/sonner.tsx +50 -0
- package/template/base/src/components/ui/spinner.tsx +18 -0
- package/template/base/src/components/ui/switch.tsx +30 -0
- package/template/base/src/components/ui/table.tsx +89 -0
- package/template/base/src/components/ui/tabs.tsx +73 -0
- package/template/base/src/components/ui/textarea.tsx +18 -0
- package/template/base/src/components/ui/toggle-group.tsx +85 -0
- package/template/base/src/components/ui/toggle.tsx +45 -0
- package/template/base/src/components/ui/toolbar.tsx +451 -0
- package/template/base/src/components/ui/tooltip.tsx +52 -0
- package/template/base/src/hooks/use-mobile.ts +19 -0
- package/template/base/src/lib/utils.ts +6 -0
- package/template/base/src/routes/__root.tsx +1 -1
- package/template/base/src/server/auth.ts +2 -2
- package/template/base/src/styles/globals.css +230 -0
- package/template/base/vite.config.ts +15 -1
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { NavigationMenu as NavigationMenuPrimitive } from '@base-ui/react/navigation-menu';
|
|
2
|
+
import { cva } from 'class-variance-authority';
|
|
3
|
+
|
|
4
|
+
import { ArrowDown01Icon } from '@hugeicons/core-free-icons';
|
|
5
|
+
import { HugeiconsIcon } from '@hugeicons/react';
|
|
6
|
+
import { cn } from '@saena-io/ui/lib/utils';
|
|
7
|
+
|
|
8
|
+
function NavigationMenu({
|
|
9
|
+
align = 'start',
|
|
10
|
+
className,
|
|
11
|
+
children,
|
|
12
|
+
...props
|
|
13
|
+
}: NavigationMenuPrimitive.Root.Props & Pick<NavigationMenuPrimitive.Positioner.Props, 'align'>) {
|
|
14
|
+
return (
|
|
15
|
+
<NavigationMenuPrimitive.Root
|
|
16
|
+
data-slot="navigation-menu"
|
|
17
|
+
className={cn(
|
|
18
|
+
'group/navigation-menu relative flex max-w-max flex-1 items-center justify-center',
|
|
19
|
+
className,
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
>
|
|
23
|
+
{children}
|
|
24
|
+
<NavigationMenuPositioner align={align} />
|
|
25
|
+
</NavigationMenuPrimitive.Root>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function NavigationMenuList({
|
|
30
|
+
className,
|
|
31
|
+
...props
|
|
32
|
+
}: React.ComponentPropsWithRef<typeof NavigationMenuPrimitive.List>) {
|
|
33
|
+
return (
|
|
34
|
+
<NavigationMenuPrimitive.List
|
|
35
|
+
data-slot="navigation-menu-list"
|
|
36
|
+
className={cn('group flex flex-1 list-none items-center justify-center gap-0', className)}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function NavigationMenuItem({
|
|
43
|
+
className,
|
|
44
|
+
...props
|
|
45
|
+
}: React.ComponentPropsWithRef<typeof NavigationMenuPrimitive.Item>) {
|
|
46
|
+
return (
|
|
47
|
+
<NavigationMenuPrimitive.Item
|
|
48
|
+
data-slot="navigation-menu-item"
|
|
49
|
+
className={cn('relative', className)}
|
|
50
|
+
{...props}
|
|
51
|
+
/>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
const navigationMenuTriggerStyle = cva(
|
|
56
|
+
'group/navigation-menu-trigger inline-flex h-9 w-max items-center justify-center rounded-lg px-2.5 py-1.5 text-xs/relaxed font-medium transition-all outline-none hover:bg-muted focus:bg-muted focus-visible:ring-2 focus-visible:ring-ring/30 focus-visible:outline-1 disabled:pointer-events-none disabled:opacity-50 data-popup-open:bg-muted/50 data-popup-open:hover:bg-muted data-open:bg-muted/50 data-open:hover:bg-muted data-open:focus:bg-muted',
|
|
57
|
+
);
|
|
58
|
+
|
|
59
|
+
function NavigationMenuTrigger({
|
|
60
|
+
className,
|
|
61
|
+
children,
|
|
62
|
+
...props
|
|
63
|
+
}: NavigationMenuPrimitive.Trigger.Props) {
|
|
64
|
+
return (
|
|
65
|
+
<NavigationMenuPrimitive.Trigger
|
|
66
|
+
data-slot="navigation-menu-trigger"
|
|
67
|
+
className={cn(navigationMenuTriggerStyle(), 'group', className)}
|
|
68
|
+
{...props}
|
|
69
|
+
>
|
|
70
|
+
{children}{' '}
|
|
71
|
+
<HugeiconsIcon
|
|
72
|
+
icon={ArrowDown01Icon}
|
|
73
|
+
strokeWidth={2}
|
|
74
|
+
className="relative top-px ml-1 size-3 transition duration-300 group-data-popup-open/navigation-menu-trigger:rotate-180 group-data-open/navigation-menu-trigger:rotate-180"
|
|
75
|
+
aria-hidden="true"
|
|
76
|
+
/>
|
|
77
|
+
</NavigationMenuPrimitive.Trigger>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function NavigationMenuContent({ className, ...props }: NavigationMenuPrimitive.Content.Props) {
|
|
82
|
+
return (
|
|
83
|
+
<NavigationMenuPrimitive.Content
|
|
84
|
+
data-slot="navigation-menu-content"
|
|
85
|
+
className={cn(
|
|
86
|
+
'data-ending-style:data-activation-direction=left:translate-x-[50%] data-ending-style:data-activation-direction=right:translate-x-[-50%] data-starting-style:data-activation-direction=left:translate-x-[-50%] data-starting-style:data-activation-direction=right:translate-x-[50%] h-full w-auto p-1.5 transition-[opacity,transform,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] group-data-[viewport=false]/navigation-menu:rounded-xl group-data-[viewport=false]/navigation-menu:bg-popover group-data-[viewport=false]/navigation-menu:text-popover-foreground group-data-[viewport=false]/navigation-menu:shadow-md group-data-[viewport=false]/navigation-menu:ring-1 group-data-[viewport=false]/navigation-menu:ring-foreground/10 group-data-[viewport=false]/navigation-menu:duration-300 data-ending-style:opacity-0 data-starting-style:opacity-0 data-[motion=from-end]:slide-in-from-right-52 data-[motion=from-start]:slide-in-from-left-52 data-[motion=to-end]:slide-out-to-right-52 data-[motion=to-start]:slide-out-to-left-52 data-[motion^=from-]:animate-in data-[motion^=from-]:fade-in data-[motion^=to-]:animate-out data-[motion^=to-]:fade-out **:data-[slot=navigation-menu-link]:focus:ring-0 **:data-[slot=navigation-menu-link]:focus:outline-none group-data-[viewport=false]/navigation-menu:data-open:animate-in group-data-[viewport=false]/navigation-menu:data-open:fade-in-0 group-data-[viewport=false]/navigation-menu:data-open:zoom-in-95 group-data-[viewport=false]/navigation-menu:data-closed:animate-out group-data-[viewport=false]/navigation-menu:data-closed:fade-out-0 group-data-[viewport=false]/navigation-menu:data-closed:zoom-out-95',
|
|
87
|
+
className,
|
|
88
|
+
)}
|
|
89
|
+
{...props}
|
|
90
|
+
/>
|
|
91
|
+
);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function NavigationMenuPositioner({
|
|
95
|
+
className,
|
|
96
|
+
side = 'bottom',
|
|
97
|
+
sideOffset = 8,
|
|
98
|
+
align = 'start',
|
|
99
|
+
alignOffset = 0,
|
|
100
|
+
...props
|
|
101
|
+
}: NavigationMenuPrimitive.Positioner.Props) {
|
|
102
|
+
return (
|
|
103
|
+
<NavigationMenuPrimitive.Portal>
|
|
104
|
+
<NavigationMenuPrimitive.Positioner
|
|
105
|
+
side={side}
|
|
106
|
+
sideOffset={sideOffset}
|
|
107
|
+
align={align}
|
|
108
|
+
alignOffset={alignOffset}
|
|
109
|
+
className={cn(
|
|
110
|
+
'isolate z-50 h-(--positioner-height) w-(--positioner-width) max-w-(--available-width) transition-[top,left,right,bottom] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] data-instant:transition-none data-[side=bottom]:before:top-[-10px] data-[side=bottom]:before:right-0 data-[side=bottom]:before:left-0',
|
|
111
|
+
className,
|
|
112
|
+
)}
|
|
113
|
+
{...props}
|
|
114
|
+
>
|
|
115
|
+
<NavigationMenuPrimitive.Popup className="data-[ending-style]:easing-[ease] xs:w-(--popup-width) relative h-(--popup-height) w-(--popup-width) origin-(--transform-origin) rounded-xl bg-popover text-popover-foreground shadow ring-1 ring-foreground/10 transition-[opacity,transform,width,height,scale,translate] duration-[0.35s] ease-[cubic-bezier(0.22,1,0.36,1)] outline-none data-ending-style:scale-90 data-ending-style:opacity-0 data-ending-style:duration-150 data-starting-style:scale-90 data-starting-style:opacity-0">
|
|
116
|
+
<NavigationMenuPrimitive.Viewport className="relative size-full overflow-hidden" />
|
|
117
|
+
</NavigationMenuPrimitive.Popup>
|
|
118
|
+
</NavigationMenuPrimitive.Positioner>
|
|
119
|
+
</NavigationMenuPrimitive.Portal>
|
|
120
|
+
);
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
function NavigationMenuLink({ className, ...props }: NavigationMenuPrimitive.Link.Props) {
|
|
124
|
+
return (
|
|
125
|
+
<NavigationMenuPrimitive.Link
|
|
126
|
+
data-slot="navigation-menu-link"
|
|
127
|
+
className={cn(
|
|
128
|
+
"flex items-center gap-1.5 rounded-lg p-2 text-xs/relaxed transition-all outline-none hover:bg-muted focus:bg-muted focus-visible:ring-2 focus-visible:ring-ring/30 focus-visible:outline-1 in-data-[slot=navigation-menu-content]:rounded-md data-[active=true]:bg-muted/50 data-[active=true]:hover:bg-muted data-[active=true]:focus:bg-muted [&_svg:not([class*='size-'])]:size-4",
|
|
129
|
+
className,
|
|
130
|
+
)}
|
|
131
|
+
{...props}
|
|
132
|
+
/>
|
|
133
|
+
);
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
function NavigationMenuIndicator({
|
|
137
|
+
className,
|
|
138
|
+
...props
|
|
139
|
+
}: React.ComponentPropsWithRef<typeof NavigationMenuPrimitive.Icon>) {
|
|
140
|
+
return (
|
|
141
|
+
<NavigationMenuPrimitive.Icon
|
|
142
|
+
data-slot="navigation-menu-indicator"
|
|
143
|
+
className={cn(
|
|
144
|
+
'top-full z-1 flex h-1.5 items-end justify-center overflow-hidden data-[state=hidden]:animate-out data-[state=hidden]:fade-out data-[state=visible]:animate-in data-[state=visible]:fade-in',
|
|
145
|
+
className,
|
|
146
|
+
)}
|
|
147
|
+
{...props}
|
|
148
|
+
>
|
|
149
|
+
<div className="relative top-[60%] h-2 w-2 rotate-45 rounded-tl-sm bg-border shadow-md" />
|
|
150
|
+
</NavigationMenuPrimitive.Icon>
|
|
151
|
+
);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
export {
|
|
155
|
+
NavigationMenu,
|
|
156
|
+
NavigationMenuContent,
|
|
157
|
+
NavigationMenuIndicator,
|
|
158
|
+
NavigationMenuItem,
|
|
159
|
+
NavigationMenuLink,
|
|
160
|
+
NavigationMenuList,
|
|
161
|
+
NavigationMenuTrigger,
|
|
162
|
+
navigationMenuTriggerStyle,
|
|
163
|
+
NavigationMenuPositioner,
|
|
164
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
import type * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
ArrowLeft01Icon,
|
|
5
|
+
ArrowRight01Icon,
|
|
6
|
+
MoreHorizontalCircle01Icon,
|
|
7
|
+
} from '@hugeicons/core-free-icons';
|
|
8
|
+
import { HugeiconsIcon } from '@hugeicons/react';
|
|
9
|
+
import { Button } from '@saena-io/ui/components/button';
|
|
10
|
+
import { cn } from '@saena-io/ui/lib/utils';
|
|
11
|
+
|
|
12
|
+
function Pagination({ className, ...props }: React.ComponentProps<'nav'>) {
|
|
13
|
+
return (
|
|
14
|
+
<nav
|
|
15
|
+
role="navigation"
|
|
16
|
+
aria-label="pagination"
|
|
17
|
+
data-slot="pagination"
|
|
18
|
+
className={cn('mx-auto flex w-full justify-center', className)}
|
|
19
|
+
{...props}
|
|
20
|
+
/>
|
|
21
|
+
);
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function PaginationContent({ className, ...props }: React.ComponentProps<'ul'>) {
|
|
25
|
+
return (
|
|
26
|
+
<ul
|
|
27
|
+
data-slot="pagination-content"
|
|
28
|
+
className={cn('flex items-center gap-0.5', className)}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function PaginationItem({ ...props }: React.ComponentProps<'li'>) {
|
|
35
|
+
return <li data-slot="pagination-item" {...props} />;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
type PaginationLinkProps = {
|
|
39
|
+
isActive?: boolean;
|
|
40
|
+
} & Pick<React.ComponentProps<typeof Button>, 'size'> &
|
|
41
|
+
React.ComponentProps<'a'>;
|
|
42
|
+
|
|
43
|
+
function PaginationLink({ className, isActive, size = 'icon', ...props }: PaginationLinkProps) {
|
|
44
|
+
return (
|
|
45
|
+
<Button
|
|
46
|
+
variant={isActive ? 'outline' : 'ghost'}
|
|
47
|
+
size={size}
|
|
48
|
+
className={cn(className)}
|
|
49
|
+
nativeButton={false}
|
|
50
|
+
render={
|
|
51
|
+
<a
|
|
52
|
+
aria-current={isActive ? 'page' : undefined}
|
|
53
|
+
data-slot="pagination-link"
|
|
54
|
+
data-active={isActive}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
}
|
|
58
|
+
/>
|
|
59
|
+
);
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function PaginationPrevious({
|
|
63
|
+
className,
|
|
64
|
+
text = 'Previous',
|
|
65
|
+
...props
|
|
66
|
+
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
|
67
|
+
return (
|
|
68
|
+
<PaginationLink
|
|
69
|
+
aria-label="Go to previous page"
|
|
70
|
+
size="default"
|
|
71
|
+
className={cn('pl-2!', className)}
|
|
72
|
+
{...props}
|
|
73
|
+
>
|
|
74
|
+
<HugeiconsIcon icon={ArrowLeft01Icon} strokeWidth={2} data-icon="inline-start" />
|
|
75
|
+
<span className="hidden sm:block">{text}</span>
|
|
76
|
+
</PaginationLink>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function PaginationNext({
|
|
81
|
+
className,
|
|
82
|
+
text = 'Next',
|
|
83
|
+
...props
|
|
84
|
+
}: React.ComponentProps<typeof PaginationLink> & { text?: string }) {
|
|
85
|
+
return (
|
|
86
|
+
<PaginationLink
|
|
87
|
+
aria-label="Go to next page"
|
|
88
|
+
size="default"
|
|
89
|
+
className={cn('pr-2!', className)}
|
|
90
|
+
{...props}
|
|
91
|
+
>
|
|
92
|
+
<span className="hidden sm:block">{text}</span>
|
|
93
|
+
<HugeiconsIcon icon={ArrowRight01Icon} strokeWidth={2} data-icon="inline-end" />
|
|
94
|
+
</PaginationLink>
|
|
95
|
+
);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
function PaginationEllipsis({ className, ...props }: React.ComponentProps<'span'>) {
|
|
99
|
+
return (
|
|
100
|
+
<span
|
|
101
|
+
aria-hidden
|
|
102
|
+
data-slot="pagination-ellipsis"
|
|
103
|
+
className={cn(
|
|
104
|
+
"flex size-7 items-center justify-center [&_svg:not([class*='size-'])]:size-3.5",
|
|
105
|
+
className,
|
|
106
|
+
)}
|
|
107
|
+
{...props}
|
|
108
|
+
>
|
|
109
|
+
<HugeiconsIcon icon={MoreHorizontalCircle01Icon} strokeWidth={2} />
|
|
110
|
+
<span className="sr-only">More pages</span>
|
|
111
|
+
</span>
|
|
112
|
+
);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
export {
|
|
116
|
+
Pagination,
|
|
117
|
+
PaginationContent,
|
|
118
|
+
PaginationEllipsis,
|
|
119
|
+
PaginationItem,
|
|
120
|
+
PaginationLink,
|
|
121
|
+
PaginationNext,
|
|
122
|
+
PaginationPrevious,
|
|
123
|
+
};
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { Popover as PopoverPrimitive } from '@base-ui/react/popover';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@saena-io/ui/lib/utils';
|
|
5
|
+
|
|
6
|
+
function Popover({ ...props }: PopoverPrimitive.Root.Props) {
|
|
7
|
+
return <PopoverPrimitive.Root data-slot="popover" {...props} />;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
function PopoverTrigger({ ...props }: PopoverPrimitive.Trigger.Props) {
|
|
11
|
+
return <PopoverPrimitive.Trigger data-slot="popover-trigger" {...props} />;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function PopoverContent({
|
|
15
|
+
className,
|
|
16
|
+
align = 'center',
|
|
17
|
+
alignOffset = 0,
|
|
18
|
+
side = 'bottom',
|
|
19
|
+
sideOffset = 4,
|
|
20
|
+
anchor,
|
|
21
|
+
...props
|
|
22
|
+
}: PopoverPrimitive.Popup.Props &
|
|
23
|
+
Pick<
|
|
24
|
+
PopoverPrimitive.Positioner.Props,
|
|
25
|
+
'align' | 'alignOffset' | 'side' | 'sideOffset' | 'anchor'
|
|
26
|
+
>) {
|
|
27
|
+
return (
|
|
28
|
+
<PopoverPrimitive.Portal>
|
|
29
|
+
<PopoverPrimitive.Positioner
|
|
30
|
+
align={align}
|
|
31
|
+
alignOffset={alignOffset}
|
|
32
|
+
side={side}
|
|
33
|
+
sideOffset={sideOffset}
|
|
34
|
+
anchor={anchor}
|
|
35
|
+
className="isolate z-50"
|
|
36
|
+
>
|
|
37
|
+
<PopoverPrimitive.Popup
|
|
38
|
+
data-slot="popover-content"
|
|
39
|
+
className={cn(
|
|
40
|
+
'z-50 flex w-72 origin-(--transform-origin) flex-col gap-4 rounded-lg bg-popover p-2.5 text-xs text-popover-foreground shadow-md ring-1 ring-foreground/10 outline-hidden duration-100 data-[side=bottom]:slide-in-from-top-2 data-[side=inline-end]:slide-in-from-left-2 data-[side=inline-start]:slide-in-from-right-2 data-[side=left]:slide-in-from-right-2 data-[side=right]:slide-in-from-left-2 data-[side=top]:slide-in-from-bottom-2 data-open:animate-in data-open:fade-in-0 data-open:zoom-in-95 data-closed:animate-out data-closed:fade-out-0 data-closed:zoom-out-95',
|
|
41
|
+
className,
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
/>
|
|
45
|
+
</PopoverPrimitive.Positioner>
|
|
46
|
+
</PopoverPrimitive.Portal>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function PopoverHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|
51
|
+
return (
|
|
52
|
+
<div
|
|
53
|
+
data-slot="popover-header"
|
|
54
|
+
className={cn('flex flex-col gap-1 text-xs', className)}
|
|
55
|
+
{...props}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function PopoverTitle({ className, ...props }: PopoverPrimitive.Title.Props) {
|
|
61
|
+
return (
|
|
62
|
+
<PopoverPrimitive.Title
|
|
63
|
+
data-slot="popover-title"
|
|
64
|
+
className={cn('text-sm font-medium', className)}
|
|
65
|
+
{...props}
|
|
66
|
+
/>
|
|
67
|
+
);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
function PopoverDescription({ className, ...props }: PopoverPrimitive.Description.Props) {
|
|
71
|
+
return (
|
|
72
|
+
<PopoverPrimitive.Description
|
|
73
|
+
data-slot="popover-description"
|
|
74
|
+
className={cn('text-muted-foreground', className)}
|
|
75
|
+
{...props}
|
|
76
|
+
/>
|
|
77
|
+
);
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
export { Popover, PopoverContent, PopoverDescription, PopoverHeader, PopoverTitle, PopoverTrigger };
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { Progress as ProgressPrimitive } from '@base-ui/react/progress';
|
|
4
|
+
|
|
5
|
+
import { cn } from '@saena-io/ui/lib/utils';
|
|
6
|
+
|
|
7
|
+
function Progress({ className, children, value, ...props }: ProgressPrimitive.Root.Props) {
|
|
8
|
+
return (
|
|
9
|
+
<ProgressPrimitive.Root
|
|
10
|
+
value={value}
|
|
11
|
+
data-slot="progress"
|
|
12
|
+
className={cn('flex flex-wrap gap-3', className)}
|
|
13
|
+
{...props}
|
|
14
|
+
>
|
|
15
|
+
{children}
|
|
16
|
+
<ProgressTrack>
|
|
17
|
+
<ProgressIndicator />
|
|
18
|
+
</ProgressTrack>
|
|
19
|
+
</ProgressPrimitive.Root>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function ProgressTrack({ className, ...props }: ProgressPrimitive.Track.Props) {
|
|
24
|
+
return (
|
|
25
|
+
<ProgressPrimitive.Track
|
|
26
|
+
className={cn(
|
|
27
|
+
'relative flex h-1 w-full items-center overflow-x-hidden rounded-md bg-muted',
|
|
28
|
+
className,
|
|
29
|
+
)}
|
|
30
|
+
data-slot="progress-track"
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function ProgressIndicator({ className, ...props }: ProgressPrimitive.Indicator.Props) {
|
|
37
|
+
return (
|
|
38
|
+
<ProgressPrimitive.Indicator
|
|
39
|
+
data-slot="progress-indicator"
|
|
40
|
+
className={cn('h-full bg-primary transition-all', className)}
|
|
41
|
+
{...props}
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function ProgressLabel({ className, ...props }: ProgressPrimitive.Label.Props) {
|
|
47
|
+
return (
|
|
48
|
+
<ProgressPrimitive.Label
|
|
49
|
+
className={cn('text-xs/relaxed font-medium', className)}
|
|
50
|
+
data-slot="progress-label"
|
|
51
|
+
{...props}
|
|
52
|
+
/>
|
|
53
|
+
);
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function ProgressValue({ className, ...props }: ProgressPrimitive.Value.Props) {
|
|
57
|
+
return (
|
|
58
|
+
<ProgressPrimitive.Value
|
|
59
|
+
className={cn('ml-auto text-xs/relaxed text-muted-foreground tabular-nums', className)}
|
|
60
|
+
data-slot="progress-value"
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export { Progress, ProgressTrack, ProgressIndicator, ProgressLabel, ProgressValue };
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { Radio as RadioPrimitive } from '@base-ui/react/radio';
|
|
2
|
+
import { RadioGroup as RadioGroupPrimitive } from '@base-ui/react/radio-group';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@saena-io/ui/lib/utils';
|
|
5
|
+
|
|
6
|
+
function RadioGroup({ className, ...props }: RadioGroupPrimitive.Props) {
|
|
7
|
+
return (
|
|
8
|
+
<RadioGroupPrimitive
|
|
9
|
+
data-slot="radio-group"
|
|
10
|
+
className={cn('grid w-full gap-3', className)}
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function RadioGroupItem({ className, ...props }: RadioPrimitive.Root.Props) {
|
|
17
|
+
return (
|
|
18
|
+
<RadioPrimitive.Root
|
|
19
|
+
data-slot="radio-group-item"
|
|
20
|
+
className={cn(
|
|
21
|
+
'group/radio-group-item peer relative flex aspect-square size-4 shrink-0 rounded-full border border-input outline-none after:absolute after:-inset-x-3 after:-inset-y-2 focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 aria-invalid:aria-checked:border-primary dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 data-checked:border-primary data-checked:bg-primary data-checked:text-primary-foreground dark:data-checked:bg-primary',
|
|
22
|
+
className,
|
|
23
|
+
)}
|
|
24
|
+
{...props}
|
|
25
|
+
>
|
|
26
|
+
<RadioPrimitive.Indicator
|
|
27
|
+
data-slot="radio-group-indicator"
|
|
28
|
+
className="flex size-4 items-center justify-center"
|
|
29
|
+
>
|
|
30
|
+
<span className="absolute top-1/2 left-1/2 size-2 -translate-x-1/2 -translate-y-1/2 rounded-full bg-primary-foreground" />
|
|
31
|
+
</RadioPrimitive.Indicator>
|
|
32
|
+
</RadioPrimitive.Root>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export { RadioGroup, RadioGroupItem };
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as ResizablePrimitive from 'react-resizable-panels';
|
|
4
|
+
|
|
5
|
+
import { cn } from '@saena-io/ui/lib/utils';
|
|
6
|
+
|
|
7
|
+
function ResizablePanelGroup({ className, ...props }: ResizablePrimitive.GroupProps) {
|
|
8
|
+
return (
|
|
9
|
+
<ResizablePrimitive.Group
|
|
10
|
+
data-slot="resizable-panel-group"
|
|
11
|
+
className={cn('flex h-full w-full aria-[orientation=vertical]:flex-col', className)}
|
|
12
|
+
{...props}
|
|
13
|
+
/>
|
|
14
|
+
);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
function ResizablePanel({ ...props }: ResizablePrimitive.PanelProps) {
|
|
18
|
+
return <ResizablePrimitive.Panel data-slot="resizable-panel" {...props} />;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function ResizableHandle({
|
|
22
|
+
withHandle,
|
|
23
|
+
className,
|
|
24
|
+
...props
|
|
25
|
+
}: ResizablePrimitive.SeparatorProps & {
|
|
26
|
+
withHandle?: boolean;
|
|
27
|
+
}) {
|
|
28
|
+
return (
|
|
29
|
+
<ResizablePrimitive.Separator
|
|
30
|
+
data-slot="resizable-handle"
|
|
31
|
+
className={cn(
|
|
32
|
+
'relative flex w-px items-center justify-center bg-border ring-offset-background after:absolute after:inset-y-0 after:left-1/2 after:w-1 after:-translate-x-1/2 focus-visible:ring-1 focus-visible:ring-ring focus-visible:outline-hidden aria-[orientation=horizontal]:h-px aria-[orientation=horizontal]:w-full aria-[orientation=horizontal]:after:left-0 aria-[orientation=horizontal]:after:h-1 aria-[orientation=horizontal]:after:w-full aria-[orientation=horizontal]:after:translate-x-0 aria-[orientation=horizontal]:after:-translate-y-1/2 [&[aria-orientation=horizontal]>div]:rotate-90',
|
|
33
|
+
className,
|
|
34
|
+
)}
|
|
35
|
+
{...props}
|
|
36
|
+
>
|
|
37
|
+
{withHandle && <div className="z-10 flex h-6 w-1 shrink-0 rounded-lg bg-border" />}
|
|
38
|
+
</ResizablePrimitive.Separator>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export { ResizableHandle, ResizablePanel, ResizablePanelGroup };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { useAIChatEditor } from '@platejs/ai/react';
|
|
4
|
+
import { usePlateEditor } from 'platejs/react';
|
|
5
|
+
import * as React from 'react';
|
|
6
|
+
import { coreRichTextPlugins } from './plugins';
|
|
7
|
+
import { RichTextStatic } from './static';
|
|
8
|
+
|
|
9
|
+
// Renders the assistant's streamed markdown as a read-only preview inside the AI menu (selection mode), so
|
|
10
|
+
// the user can review before replacing the selection / inserting below. `useAIChatEditor` deserializes the
|
|
11
|
+
// markdown into a Plate value (using the editor's MarkdownPlugin) AND registers that editor as the plugin's
|
|
12
|
+
// `aiEditor` option — which the menu's "Replace selection" / "Insert below" transforms read. We render the
|
|
13
|
+
// value through SAENA's existing static renderer (RichTextStatic) rather than vendoring plate-ui's
|
|
14
|
+
// EditorStatic; the live `aiEditor` is only used for deserialize + as the transform source, never rendered.
|
|
15
|
+
|
|
16
|
+
export const AiChatEditor = React.memo(function AiChatEditor({ content }: { content: string }) {
|
|
17
|
+
const aiEditor = usePlateEditor({ plugins: coreRichTextPlugins });
|
|
18
|
+
const value = useAIChatEditor(aiEditor, content);
|
|
19
|
+
return <RichTextStatic value={value} className="max-h-64 overflow-y-auto px-3 py-2" />;
|
|
20
|
+
});
|
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { BaseAIPlugin } from '@platejs/ai';
|
|
4
|
+
import {
|
|
5
|
+
AIChatPlugin,
|
|
6
|
+
AIPlugin,
|
|
7
|
+
getInsertPreviewStart,
|
|
8
|
+
streamInsertChunk,
|
|
9
|
+
useChatChunk,
|
|
10
|
+
} from '@platejs/ai/react';
|
|
11
|
+
import { ElementApi, KEYS, PathApi, getPluginType } from 'platejs';
|
|
12
|
+
import { usePluginOption } from 'platejs/react';
|
|
13
|
+
import { AiLoadingBar, AiMenu } from './ai-menu';
|
|
14
|
+
import { AIAnchorElement, AILeaf } from './ai-node';
|
|
15
|
+
import { AiToolbarButton } from './ai-toolbar-button';
|
|
16
|
+
import type { RichTextExtension } from './extension';
|
|
17
|
+
import { useAiChat } from './use-ai-chat';
|
|
18
|
+
|
|
19
|
+
// The AI command-menu extension (ADR-0010), the streaming sibling of the ghost-text copilot. It wires
|
|
20
|
+
// @platejs/ai's AIPlugin (the AI leaf) + AIChatPlugin (the menu) into the editor and contributes a toolbar
|
|
21
|
+
// button in the reserved 'ai' slot. The plugin's `useHooks` owns the @ai-sdk/react chat instance (use-ai-chat,
|
|
22
|
+
// injected via setOption) and runs the insert-mode streaming engine (useChatChunk → beginPreview +
|
|
23
|
+
// streamInsertChunk), ported from @platejs/ai's example and scoped to insert mode — selection results are
|
|
24
|
+
// reviewed in the menu (AiChatEditor) and committed via replaceSelection/insertBelow, so no suggestion-diff
|
|
25
|
+
// plugin is needed. Endpoint is injected (default /api/ai/command), keeping @saena-io/ai/platejs off the plugin
|
|
26
|
+
// boundary — the same DI precedent as createAiCopilotExtension.
|
|
27
|
+
|
|
28
|
+
export interface AiCommandOptions {
|
|
29
|
+
/** Streaming endpoint the chat POSTs to (AI-SDK UI Message Stream). Default: the SAENA command route. */
|
|
30
|
+
api?: string;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function createAiCommandExtension(options: AiCommandOptions = {}): RichTextExtension {
|
|
34
|
+
const endpoint = options.api ?? '/api/ai/command';
|
|
35
|
+
return {
|
|
36
|
+
// Distinct id from the copilot's 'ai' so the two extensions never collide in the addon registry.
|
|
37
|
+
id: 'ai-command',
|
|
38
|
+
platePlugins: [
|
|
39
|
+
AIPlugin.withComponent(AILeaf),
|
|
40
|
+
AIChatPlugin.configure({
|
|
41
|
+
render: {
|
|
42
|
+
node: AIAnchorElement,
|
|
43
|
+
afterEditable: AiMenu,
|
|
44
|
+
afterContainer: AiLoadingBar,
|
|
45
|
+
},
|
|
46
|
+
useHooks: ({ editor, getOption }) => {
|
|
47
|
+
// Create + inject the chat instance (owned here, borrowed by the plugin).
|
|
48
|
+
useAiChat(editor, endpoint);
|
|
49
|
+
|
|
50
|
+
const mode = usePluginOption(AIChatPlugin, 'mode');
|
|
51
|
+
useChatChunk({
|
|
52
|
+
onChunk: ({ chunk, isFirst, nodes }) => {
|
|
53
|
+
if (isFirst && mode === 'insert') {
|
|
54
|
+
const { startBlock, startInEmptyParagraph } = getInsertPreviewStart(editor);
|
|
55
|
+
editor.getTransforms(BaseAIPlugin).ai.beginPreview({
|
|
56
|
+
originalBlocks:
|
|
57
|
+
startInEmptyParagraph && startBlock && ElementApi.isElement(startBlock)
|
|
58
|
+
? [structuredClone(startBlock)]
|
|
59
|
+
: [],
|
|
60
|
+
});
|
|
61
|
+
editor.tf.withoutSaving(() => {
|
|
62
|
+
editor.tf.insertNodes(
|
|
63
|
+
{ children: [{ text: '' }], type: getPluginType(editor, KEYS.aiChat) },
|
|
64
|
+
{ at: PathApi.next(editor.selection!.focus.path.slice(0, 1)) },
|
|
65
|
+
);
|
|
66
|
+
});
|
|
67
|
+
editor.setOption(AIChatPlugin, 'streaming', true);
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (mode === 'insert' && nodes.length > 0) {
|
|
71
|
+
editor.tf.withoutSaving(() => {
|
|
72
|
+
if (!getOption('streaming')) return;
|
|
73
|
+
editor.tf.withScrolling(() => {
|
|
74
|
+
streamInsertChunk(editor, chunk, {
|
|
75
|
+
textProps: { [getPluginType(editor, KEYS.ai)]: true },
|
|
76
|
+
});
|
|
77
|
+
});
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
},
|
|
81
|
+
onFinish: () => {
|
|
82
|
+
editor.getApi(AIChatPlugin).aiChat.stop();
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
},
|
|
86
|
+
}),
|
|
87
|
+
],
|
|
88
|
+
toolbar: [{ id: 'ai-command', slot: 'ai', order: 10, render: () => <AiToolbarButton /> }],
|
|
89
|
+
};
|
|
90
|
+
}
|