@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,183 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import type * as React from 'react';
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
BookOpen02Icon,
|
|
7
|
+
ChartRingIcon,
|
|
8
|
+
CommandIcon,
|
|
9
|
+
ComputerTerminalIcon,
|
|
10
|
+
CropIcon,
|
|
11
|
+
MapsIcon,
|
|
12
|
+
PieChartIcon,
|
|
13
|
+
RoboticIcon,
|
|
14
|
+
SentIcon,
|
|
15
|
+
Settings05Icon,
|
|
16
|
+
} from '@hugeicons/core-free-icons';
|
|
17
|
+
import { HugeiconsIcon } from '@hugeicons/react';
|
|
18
|
+
import { NavMain } from '@saena-io/ui/components/nav-main';
|
|
19
|
+
import { NavProjects } from '@saena-io/ui/components/nav-projects';
|
|
20
|
+
import { NavSecondary } from '@saena-io/ui/components/nav-secondary';
|
|
21
|
+
import { NavUser } from '@saena-io/ui/components/nav-user';
|
|
22
|
+
import {
|
|
23
|
+
Sidebar,
|
|
24
|
+
SidebarContent,
|
|
25
|
+
SidebarFooter,
|
|
26
|
+
SidebarHeader,
|
|
27
|
+
SidebarMenu,
|
|
28
|
+
SidebarMenuButton,
|
|
29
|
+
SidebarMenuItem,
|
|
30
|
+
} from '@saena-io/ui/components/sidebar';
|
|
31
|
+
|
|
32
|
+
const data = {
|
|
33
|
+
user: {
|
|
34
|
+
name: 'shadcn',
|
|
35
|
+
email: 'm@example.com',
|
|
36
|
+
avatar: '/avatars/shadcn.jpg',
|
|
37
|
+
},
|
|
38
|
+
navMain: [
|
|
39
|
+
{
|
|
40
|
+
title: 'Playground',
|
|
41
|
+
url: '#',
|
|
42
|
+
icon: <HugeiconsIcon icon={ComputerTerminalIcon} strokeWidth={2} />,
|
|
43
|
+
isActive: true,
|
|
44
|
+
items: [
|
|
45
|
+
{
|
|
46
|
+
title: 'History',
|
|
47
|
+
url: '#',
|
|
48
|
+
},
|
|
49
|
+
{
|
|
50
|
+
title: 'Starred',
|
|
51
|
+
url: '#',
|
|
52
|
+
},
|
|
53
|
+
{
|
|
54
|
+
title: 'Settings',
|
|
55
|
+
url: '#',
|
|
56
|
+
},
|
|
57
|
+
],
|
|
58
|
+
},
|
|
59
|
+
{
|
|
60
|
+
title: 'Models',
|
|
61
|
+
url: '#',
|
|
62
|
+
icon: <HugeiconsIcon icon={RoboticIcon} strokeWidth={2} />,
|
|
63
|
+
items: [
|
|
64
|
+
{
|
|
65
|
+
title: 'Genesis',
|
|
66
|
+
url: '#',
|
|
67
|
+
},
|
|
68
|
+
{
|
|
69
|
+
title: 'Explorer',
|
|
70
|
+
url: '#',
|
|
71
|
+
},
|
|
72
|
+
{
|
|
73
|
+
title: 'Quantum',
|
|
74
|
+
url: '#',
|
|
75
|
+
},
|
|
76
|
+
],
|
|
77
|
+
},
|
|
78
|
+
{
|
|
79
|
+
title: 'Documentation',
|
|
80
|
+
url: '#',
|
|
81
|
+
icon: <HugeiconsIcon icon={BookOpen02Icon} strokeWidth={2} />,
|
|
82
|
+
items: [
|
|
83
|
+
{
|
|
84
|
+
title: 'Introduction',
|
|
85
|
+
url: '#',
|
|
86
|
+
},
|
|
87
|
+
{
|
|
88
|
+
title: 'Get Started',
|
|
89
|
+
url: '#',
|
|
90
|
+
},
|
|
91
|
+
{
|
|
92
|
+
title: 'Tutorials',
|
|
93
|
+
url: '#',
|
|
94
|
+
},
|
|
95
|
+
{
|
|
96
|
+
title: 'Changelog',
|
|
97
|
+
url: '#',
|
|
98
|
+
},
|
|
99
|
+
],
|
|
100
|
+
},
|
|
101
|
+
{
|
|
102
|
+
title: 'Settings',
|
|
103
|
+
url: '#',
|
|
104
|
+
icon: <HugeiconsIcon icon={Settings05Icon} strokeWidth={2} />,
|
|
105
|
+
items: [
|
|
106
|
+
{
|
|
107
|
+
title: 'General',
|
|
108
|
+
url: '#',
|
|
109
|
+
},
|
|
110
|
+
{
|
|
111
|
+
title: 'Team',
|
|
112
|
+
url: '#',
|
|
113
|
+
},
|
|
114
|
+
{
|
|
115
|
+
title: 'Billing',
|
|
116
|
+
url: '#',
|
|
117
|
+
},
|
|
118
|
+
{
|
|
119
|
+
title: 'Limits',
|
|
120
|
+
url: '#',
|
|
121
|
+
},
|
|
122
|
+
],
|
|
123
|
+
},
|
|
124
|
+
],
|
|
125
|
+
navSecondary: [
|
|
126
|
+
{
|
|
127
|
+
title: 'Support',
|
|
128
|
+
url: '#',
|
|
129
|
+
icon: <HugeiconsIcon icon={ChartRingIcon} strokeWidth={2} />,
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
title: 'Feedback',
|
|
133
|
+
url: '#',
|
|
134
|
+
icon: <HugeiconsIcon icon={SentIcon} strokeWidth={2} />,
|
|
135
|
+
},
|
|
136
|
+
],
|
|
137
|
+
projects: [
|
|
138
|
+
{
|
|
139
|
+
name: 'Design Engineering',
|
|
140
|
+
url: '#',
|
|
141
|
+
icon: <HugeiconsIcon icon={CropIcon} strokeWidth={2} />,
|
|
142
|
+
},
|
|
143
|
+
{
|
|
144
|
+
name: 'Sales & Marketing',
|
|
145
|
+
url: '#',
|
|
146
|
+
icon: <HugeiconsIcon icon={PieChartIcon} strokeWidth={2} />,
|
|
147
|
+
},
|
|
148
|
+
{
|
|
149
|
+
name: 'Travel',
|
|
150
|
+
url: '#',
|
|
151
|
+
icon: <HugeiconsIcon icon={MapsIcon} strokeWidth={2} />,
|
|
152
|
+
},
|
|
153
|
+
],
|
|
154
|
+
};
|
|
155
|
+
export function AppSidebar({ ...props }: React.ComponentProps<typeof Sidebar>) {
|
|
156
|
+
return (
|
|
157
|
+
<Sidebar variant="inset" {...props}>
|
|
158
|
+
<SidebarHeader>
|
|
159
|
+
<SidebarMenu>
|
|
160
|
+
<SidebarMenuItem>
|
|
161
|
+
<SidebarMenuButton size="lg" render={<a href="#" />}>
|
|
162
|
+
<div className="flex aspect-square size-8 items-center justify-center rounded-lg bg-sidebar-primary text-sidebar-primary-foreground">
|
|
163
|
+
<HugeiconsIcon icon={CommandIcon} strokeWidth={2} className="size-4" />
|
|
164
|
+
</div>
|
|
165
|
+
<div className="grid flex-1 text-left text-sm leading-tight">
|
|
166
|
+
<span className="truncate font-medium">Acme Inc</span>
|
|
167
|
+
<span className="truncate text-xs">Enterprise</span>
|
|
168
|
+
</div>
|
|
169
|
+
</SidebarMenuButton>
|
|
170
|
+
</SidebarMenuItem>
|
|
171
|
+
</SidebarMenu>
|
|
172
|
+
</SidebarHeader>
|
|
173
|
+
<SidebarContent>
|
|
174
|
+
<NavMain items={data.navMain} />
|
|
175
|
+
<NavProjects projects={data.projects} />
|
|
176
|
+
<NavSecondary items={data.navSecondary} className="mt-auto" />
|
|
177
|
+
</SidebarContent>
|
|
178
|
+
<SidebarFooter>
|
|
179
|
+
<NavUser user={data.user} />
|
|
180
|
+
</SidebarFooter>
|
|
181
|
+
</Sidebar>
|
|
182
|
+
);
|
|
183
|
+
}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
import { cn } from '@saena-io/ui/lib/utils';
|
|
2
|
+
|
|
3
|
+
function AspectRatio({
|
|
4
|
+
ratio,
|
|
5
|
+
className,
|
|
6
|
+
...props
|
|
7
|
+
}: React.ComponentProps<'div'> & { ratio: number }) {
|
|
8
|
+
return (
|
|
9
|
+
<div
|
|
10
|
+
data-slot="aspect-ratio"
|
|
11
|
+
style={
|
|
12
|
+
{
|
|
13
|
+
'--ratio': ratio,
|
|
14
|
+
} as React.CSSProperties
|
|
15
|
+
}
|
|
16
|
+
className={cn('relative aspect-(--ratio)', className)}
|
|
17
|
+
{...props}
|
|
18
|
+
/>
|
|
19
|
+
);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
export { AspectRatio };
|
|
@@ -0,0 +1,211 @@
|
|
|
1
|
+
import { ImageUpload01Icon } from '@hugeicons/core-free-icons';
|
|
2
|
+
import { HugeiconsIcon } from '@hugeicons/react';
|
|
3
|
+
import type { AssetRef } from '@saena-io/plugin-sdk';
|
|
4
|
+
import { cn } from '@saena-io/ui/lib/utils';
|
|
5
|
+
import { type ChangeEvent, type DragEvent, useEffect, useMemo, useRef, useState } from 'react';
|
|
6
|
+
import { Button } from './button';
|
|
7
|
+
import { Spinner } from './spinner';
|
|
8
|
+
|
|
9
|
+
// The core asset uploader (ADR-0002): a controlled field that POSTs a file to the host's upload route and yields
|
|
10
|
+
// an AssetRef. A future DAM plugin registers a richer picker that replaces this everywhere it appears.
|
|
11
|
+
|
|
12
|
+
const UPLOAD_URL = '/api/assets/upload';
|
|
13
|
+
const DEFAULT_ACCEPT = 'image/png,image/jpeg,image/webp,image/gif,image/avif';
|
|
14
|
+
|
|
15
|
+
function formatBytes(n: number): string {
|
|
16
|
+
if (n < 1024) return `${n} B`;
|
|
17
|
+
if (n < 1024 * 1024) return `${Math.round(n / 1024)} KB`;
|
|
18
|
+
return `${(n / (1024 * 1024)).toFixed(1)} MB`;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export function AssetInput({
|
|
22
|
+
value,
|
|
23
|
+
onChange,
|
|
24
|
+
onEdit,
|
|
25
|
+
accept = DEFAULT_ACCEPT,
|
|
26
|
+
className,
|
|
27
|
+
showRemove = true,
|
|
28
|
+
}: {
|
|
29
|
+
value: AssetRef | null;
|
|
30
|
+
onChange: (ref: AssetRef | null) => void;
|
|
31
|
+
/** When set, the filled control shows an "Edit" action (e.g. to open a focal-point/alt sheet). */
|
|
32
|
+
onEdit?: () => void;
|
|
33
|
+
accept?: string;
|
|
34
|
+
className?: string;
|
|
35
|
+
/** Show the "Remove" (clear) button when filled. Off when removal is owned by a container (e.g. a list row). */
|
|
36
|
+
showRemove?: boolean;
|
|
37
|
+
}) {
|
|
38
|
+
const inputRef = useRef<HTMLInputElement>(null);
|
|
39
|
+
const [uploading, setUploading] = useState(false);
|
|
40
|
+
const [error, setError] = useState<string | null>(null);
|
|
41
|
+
const [dragging, setDragging] = useState(false);
|
|
42
|
+
// Drag-enter/leave fire per child element; count depth so the highlight doesn't flicker over inner nodes.
|
|
43
|
+
const dragDepth = useRef(0);
|
|
44
|
+
const acceptedTypes = useMemo(
|
|
45
|
+
() =>
|
|
46
|
+
new Set(
|
|
47
|
+
accept
|
|
48
|
+
.split(',')
|
|
49
|
+
.map((s) => s.trim())
|
|
50
|
+
.filter(Boolean),
|
|
51
|
+
),
|
|
52
|
+
[accept],
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
// A drag cancelled (Escape) or dropped outside this element fires no local dragleave/drop, so reset the
|
|
56
|
+
// highlight on any window-level drag end while we're highlighted.
|
|
57
|
+
useEffect(() => {
|
|
58
|
+
if (!dragging) return;
|
|
59
|
+
const reset = () => {
|
|
60
|
+
dragDepth.current = 0;
|
|
61
|
+
setDragging(false);
|
|
62
|
+
};
|
|
63
|
+
window.addEventListener('dragend', reset);
|
|
64
|
+
window.addEventListener('drop', reset);
|
|
65
|
+
return () => {
|
|
66
|
+
window.removeEventListener('dragend', reset);
|
|
67
|
+
window.removeEventListener('drop', reset);
|
|
68
|
+
};
|
|
69
|
+
}, [dragging]);
|
|
70
|
+
|
|
71
|
+
const pick = () => inputRef.current?.click();
|
|
72
|
+
|
|
73
|
+
async function upload(file: File): Promise<void> {
|
|
74
|
+
setUploading(true);
|
|
75
|
+
setError(null);
|
|
76
|
+
try {
|
|
77
|
+
const body = new FormData();
|
|
78
|
+
body.append('file', file);
|
|
79
|
+
const res = await fetch(UPLOAD_URL, { method: 'POST', body });
|
|
80
|
+
if (!res.ok) {
|
|
81
|
+
const detail = (await res.json().catch(() => null)) as { error?: string } | null;
|
|
82
|
+
throw new Error(detail?.error ?? `upload failed (${res.status})`);
|
|
83
|
+
}
|
|
84
|
+
onChange((await res.json()) as AssetRef);
|
|
85
|
+
} catch (e) {
|
|
86
|
+
setError(e instanceof Error ? e.message : 'upload failed');
|
|
87
|
+
} finally {
|
|
88
|
+
setUploading(false);
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function onFile(e: ChangeEvent<HTMLInputElement>): void {
|
|
93
|
+
const file = e.target.files?.[0];
|
|
94
|
+
e.target.value = ''; // let the same file be re-picked after a remove
|
|
95
|
+
if (file) void upload(file);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
// Native drag-and-drop: the whole control is a drop target (empty or filled — a drop replaces). Disabled
|
|
99
|
+
// mid-upload. Every handler ignores non-file drags (`types` lacks 'Files') so e.g. a list-item reorder drag
|
|
100
|
+
// passes straight through to the list without flickering this dropzone.
|
|
101
|
+
const isFileDrag = (e: DragEvent) => e.dataTransfer.types.includes('Files');
|
|
102
|
+
const dropHandlers = uploading
|
|
103
|
+
? {}
|
|
104
|
+
: {
|
|
105
|
+
onDragEnter: (e: DragEvent) => {
|
|
106
|
+
if (!isFileDrag(e)) return;
|
|
107
|
+
e.preventDefault();
|
|
108
|
+
dragDepth.current += 1;
|
|
109
|
+
setDragging(true);
|
|
110
|
+
},
|
|
111
|
+
onDragOver: (e: DragEvent) => {
|
|
112
|
+
if (!isFileDrag(e)) return;
|
|
113
|
+
e.preventDefault();
|
|
114
|
+
e.dataTransfer.dropEffect = 'copy';
|
|
115
|
+
},
|
|
116
|
+
onDragLeave: (e: DragEvent) => {
|
|
117
|
+
if (!isFileDrag(e)) return;
|
|
118
|
+
e.preventDefault();
|
|
119
|
+
dragDepth.current -= 1;
|
|
120
|
+
if (dragDepth.current <= 0) {
|
|
121
|
+
dragDepth.current = 0;
|
|
122
|
+
setDragging(false);
|
|
123
|
+
}
|
|
124
|
+
},
|
|
125
|
+
onDrop: (e: DragEvent) => {
|
|
126
|
+
if (!isFileDrag(e)) return;
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
dragDepth.current = 0;
|
|
129
|
+
setDragging(false);
|
|
130
|
+
const file = e.dataTransfer.files?.[0];
|
|
131
|
+
if (!file) return;
|
|
132
|
+
if (acceptedTypes.size > 0 && !acceptedTypes.has(file.type)) {
|
|
133
|
+
setError('Please drop an image');
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
void upload(file);
|
|
137
|
+
},
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
return (
|
|
141
|
+
<div className={cn('flex flex-col gap-1.5', className)} {...dropHandlers}>
|
|
142
|
+
<input ref={inputRef} type="file" accept={accept} className="hidden" onChange={onFile} />
|
|
143
|
+
{value ? (
|
|
144
|
+
<div
|
|
145
|
+
className={cn(
|
|
146
|
+
'flex items-center gap-3 rounded-md border border-input bg-input/20 p-2 transition-colors dark:bg-input/30',
|
|
147
|
+
dragging && 'border-ring ring-2 ring-ring/30',
|
|
148
|
+
)}
|
|
149
|
+
>
|
|
150
|
+
<img
|
|
151
|
+
src={value.url}
|
|
152
|
+
alt=""
|
|
153
|
+
className="size-16 shrink-0 rounded object-cover ring-1 ring-foreground/10"
|
|
154
|
+
/>
|
|
155
|
+
<div className="flex min-w-0 flex-1 flex-col gap-1.5">
|
|
156
|
+
<span className="truncate text-muted-foreground text-xs">
|
|
157
|
+
{value.mime} · {formatBytes(value.size)}
|
|
158
|
+
</span>
|
|
159
|
+
<div className="flex gap-2">
|
|
160
|
+
{onEdit ? (
|
|
161
|
+
<Button
|
|
162
|
+
type="button"
|
|
163
|
+
size="sm"
|
|
164
|
+
variant="outline"
|
|
165
|
+
onClick={onEdit}
|
|
166
|
+
disabled={uploading}
|
|
167
|
+
>
|
|
168
|
+
Edit
|
|
169
|
+
</Button>
|
|
170
|
+
) : null}
|
|
171
|
+
<Button type="button" size="sm" variant="outline" onClick={pick} disabled={uploading}>
|
|
172
|
+
{uploading ? 'Uploading…' : 'Replace'}
|
|
173
|
+
</Button>
|
|
174
|
+
{showRemove ? (
|
|
175
|
+
<Button
|
|
176
|
+
type="button"
|
|
177
|
+
size="sm"
|
|
178
|
+
variant="ghost"
|
|
179
|
+
onClick={() => onChange(null)}
|
|
180
|
+
disabled={uploading}
|
|
181
|
+
>
|
|
182
|
+
Remove
|
|
183
|
+
</Button>
|
|
184
|
+
) : null}
|
|
185
|
+
</div>
|
|
186
|
+
</div>
|
|
187
|
+
</div>
|
|
188
|
+
) : (
|
|
189
|
+
<button
|
|
190
|
+
type="button"
|
|
191
|
+
onClick={pick}
|
|
192
|
+
disabled={uploading}
|
|
193
|
+
className={cn(
|
|
194
|
+
'flex h-24 flex-col items-center justify-center gap-1.5 rounded-md border border-input border-dashed bg-input/20 text-muted-foreground text-xs transition-colors hover:bg-input/40 disabled:pointer-events-none disabled:opacity-50 dark:bg-input/30',
|
|
195
|
+
dragging && 'border-ring border-solid bg-input/40 text-foreground',
|
|
196
|
+
)}
|
|
197
|
+
>
|
|
198
|
+
{uploading ? (
|
|
199
|
+
<Spinner className="size-5" />
|
|
200
|
+
) : (
|
|
201
|
+
<HugeiconsIcon icon={ImageUpload01Icon} strokeWidth={2} className="size-5" />
|
|
202
|
+
)}
|
|
203
|
+
<span>
|
|
204
|
+
{uploading ? 'Uploading…' : dragging ? 'Drop to upload' : 'Click or drag an image here'}
|
|
205
|
+
</span>
|
|
206
|
+
</button>
|
|
207
|
+
)}
|
|
208
|
+
{error ? <span className="text-destructive text-xs">{error}</span> : null}
|
|
209
|
+
</div>
|
|
210
|
+
);
|
|
211
|
+
}
|
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
import { Avatar as AvatarPrimitive } from '@base-ui/react/avatar';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@saena-io/ui/lib/utils';
|
|
5
|
+
|
|
6
|
+
function Avatar({
|
|
7
|
+
className,
|
|
8
|
+
size = 'default',
|
|
9
|
+
...props
|
|
10
|
+
}: AvatarPrimitive.Root.Props & {
|
|
11
|
+
size?: 'default' | 'sm' | 'lg';
|
|
12
|
+
}) {
|
|
13
|
+
return (
|
|
14
|
+
<AvatarPrimitive.Root
|
|
15
|
+
data-slot="avatar"
|
|
16
|
+
data-size={size}
|
|
17
|
+
className={cn(
|
|
18
|
+
'group/avatar relative flex size-8 shrink-0 rounded-full select-none after:absolute after:inset-0 after:rounded-full after:border after:border-border after:mix-blend-darken data-[size=lg]:size-10 data-[size=sm]:size-6 dark:after:mix-blend-lighten',
|
|
19
|
+
className,
|
|
20
|
+
)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function AvatarImage({ className, ...props }: AvatarPrimitive.Image.Props) {
|
|
27
|
+
return (
|
|
28
|
+
<AvatarPrimitive.Image
|
|
29
|
+
data-slot="avatar-image"
|
|
30
|
+
className={cn('aspect-square size-full rounded-full object-cover', className)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function AvatarFallback({ className, ...props }: AvatarPrimitive.Fallback.Props) {
|
|
37
|
+
return (
|
|
38
|
+
<AvatarPrimitive.Fallback
|
|
39
|
+
data-slot="avatar-fallback"
|
|
40
|
+
className={cn(
|
|
41
|
+
'flex size-full items-center justify-center rounded-full bg-muted text-sm text-muted-foreground group-data-[size=sm]/avatar:text-xs',
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function AvatarBadge({ className, ...props }: React.ComponentProps<'span'>) {
|
|
50
|
+
return (
|
|
51
|
+
<span
|
|
52
|
+
data-slot="avatar-badge"
|
|
53
|
+
className={cn(
|
|
54
|
+
'absolute right-0 bottom-0 z-10 inline-flex items-center justify-center rounded-full bg-primary text-primary-foreground bg-blend-color ring-2 ring-background select-none',
|
|
55
|
+
'group-data-[size=sm]/avatar:size-2 group-data-[size=sm]/avatar:[&>svg]:hidden',
|
|
56
|
+
'group-data-[size=default]/avatar:size-2.5 group-data-[size=default]/avatar:[&>svg]:size-2',
|
|
57
|
+
'group-data-[size=lg]/avatar:size-3 group-data-[size=lg]/avatar:[&>svg]:size-2',
|
|
58
|
+
className,
|
|
59
|
+
)}
|
|
60
|
+
{...props}
|
|
61
|
+
/>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function AvatarGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
|
66
|
+
return (
|
|
67
|
+
<div
|
|
68
|
+
data-slot="avatar-group"
|
|
69
|
+
className={cn(
|
|
70
|
+
'group/avatar-group flex -space-x-2 *:data-[slot=avatar]:ring-2 *:data-[slot=avatar]:ring-background',
|
|
71
|
+
className,
|
|
72
|
+
)}
|
|
73
|
+
{...props}
|
|
74
|
+
/>
|
|
75
|
+
);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function AvatarGroupCount({ className, ...props }: React.ComponentProps<'div'>) {
|
|
79
|
+
return (
|
|
80
|
+
<div
|
|
81
|
+
data-slot="avatar-group-count"
|
|
82
|
+
className={cn(
|
|
83
|
+
'relative flex size-8 shrink-0 items-center justify-center rounded-full bg-muted text-xs/relaxed text-muted-foreground ring-2 ring-background group-has-data-[size=lg]/avatar-group:size-10 group-has-data-[size=sm]/avatar-group:size-6 [&>svg]:size-4 group-has-data-[size=lg]/avatar-group:[&>svg]:size-5 group-has-data-[size=sm]/avatar-group:[&>svg]:size-3',
|
|
84
|
+
className,
|
|
85
|
+
)}
|
|
86
|
+
{...props}
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
export { Avatar, AvatarImage, AvatarFallback, AvatarGroup, AvatarGroupCount, AvatarBadge };
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
import { mergeProps } from '@base-ui/react/merge-props';
|
|
2
|
+
import { useRender } from '@base-ui/react/use-render';
|
|
3
|
+
import { type VariantProps, cva } from 'class-variance-authority';
|
|
4
|
+
|
|
5
|
+
import { cn } from '@saena-io/ui/lib/utils';
|
|
6
|
+
|
|
7
|
+
const badgeVariants = cva(
|
|
8
|
+
'group/badge inline-flex h-5 w-fit shrink-0 items-center justify-center gap-1 overflow-hidden rounded-full border border-transparent px-2 py-0.5 text-[0.625rem] font-medium whitespace-nowrap transition-all focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40 [&>svg]:pointer-events-none [&>svg]:size-2.5!',
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
|
|
13
|
+
secondary: 'bg-secondary text-secondary-foreground [a]:hover:bg-secondary/80',
|
|
14
|
+
destructive:
|
|
15
|
+
'bg-destructive/10 text-destructive focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:focus-visible:ring-destructive/40 [a]:hover:bg-destructive/20',
|
|
16
|
+
outline:
|
|
17
|
+
'border-border bg-input/20 text-foreground dark:bg-input/30 [a]:hover:bg-muted [a]:hover:text-muted-foreground',
|
|
18
|
+
ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
|
|
19
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
20
|
+
},
|
|
21
|
+
},
|
|
22
|
+
defaultVariants: {
|
|
23
|
+
variant: 'default',
|
|
24
|
+
},
|
|
25
|
+
},
|
|
26
|
+
);
|
|
27
|
+
|
|
28
|
+
function Badge({
|
|
29
|
+
className,
|
|
30
|
+
variant = 'default',
|
|
31
|
+
render,
|
|
32
|
+
...props
|
|
33
|
+
}: useRender.ComponentProps<'span'> & VariantProps<typeof badgeVariants>) {
|
|
34
|
+
return useRender({
|
|
35
|
+
defaultTagName: 'span',
|
|
36
|
+
props: mergeProps<'span'>(
|
|
37
|
+
{
|
|
38
|
+
className: cn(badgeVariants({ variant }), className),
|
|
39
|
+
},
|
|
40
|
+
props,
|
|
41
|
+
),
|
|
42
|
+
render,
|
|
43
|
+
state: {
|
|
44
|
+
slot: 'badge',
|
|
45
|
+
variant,
|
|
46
|
+
},
|
|
47
|
+
});
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { mergeProps } from '@base-ui/react/merge-props';
|
|
2
|
+
import { useRender } from '@base-ui/react/use-render';
|
|
3
|
+
import type * as React from 'react';
|
|
4
|
+
|
|
5
|
+
import { ArrowRight01Icon, MoreHorizontalCircle01Icon } from '@hugeicons/core-free-icons';
|
|
6
|
+
import { HugeiconsIcon } from '@hugeicons/react';
|
|
7
|
+
import { cn } from '@saena-io/ui/lib/utils';
|
|
8
|
+
|
|
9
|
+
function Breadcrumb({ className, ...props }: React.ComponentProps<'nav'>) {
|
|
10
|
+
return (
|
|
11
|
+
<nav aria-label="breadcrumb" data-slot="breadcrumb" className={cn(className)} {...props} />
|
|
12
|
+
);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function BreadcrumbList({ className, ...props }: React.ComponentProps<'ol'>) {
|
|
16
|
+
return (
|
|
17
|
+
<ol
|
|
18
|
+
data-slot="breadcrumb-list"
|
|
19
|
+
className={cn(
|
|
20
|
+
'flex flex-wrap items-center gap-1.5 text-xs/relaxed wrap-break-word text-muted-foreground',
|
|
21
|
+
className,
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function BreadcrumbItem({ className, ...props }: React.ComponentProps<'li'>) {
|
|
29
|
+
return (
|
|
30
|
+
<li
|
|
31
|
+
data-slot="breadcrumb-item"
|
|
32
|
+
className={cn('inline-flex items-center gap-1', className)}
|
|
33
|
+
{...props}
|
|
34
|
+
/>
|
|
35
|
+
);
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
function BreadcrumbLink({ className, render, ...props }: useRender.ComponentProps<'a'>) {
|
|
39
|
+
return useRender({
|
|
40
|
+
defaultTagName: 'a',
|
|
41
|
+
props: mergeProps<'a'>(
|
|
42
|
+
{
|
|
43
|
+
className: cn('transition-colors hover:text-foreground', className),
|
|
44
|
+
},
|
|
45
|
+
props,
|
|
46
|
+
),
|
|
47
|
+
render,
|
|
48
|
+
state: {
|
|
49
|
+
slot: 'breadcrumb-link',
|
|
50
|
+
},
|
|
51
|
+
});
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function BreadcrumbPage({ className, ...props }: React.ComponentProps<'span'>) {
|
|
55
|
+
return (
|
|
56
|
+
<span
|
|
57
|
+
data-slot="breadcrumb-page"
|
|
58
|
+
role="link"
|
|
59
|
+
aria-disabled="true"
|
|
60
|
+
aria-current="page"
|
|
61
|
+
className={cn('font-normal text-foreground', className)}
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
function BreadcrumbSeparator({ children, className, ...props }: React.ComponentProps<'li'>) {
|
|
68
|
+
return (
|
|
69
|
+
<li
|
|
70
|
+
data-slot="breadcrumb-separator"
|
|
71
|
+
role="presentation"
|
|
72
|
+
aria-hidden="true"
|
|
73
|
+
className={cn('[&>svg]:size-3.5', className)}
|
|
74
|
+
{...props}
|
|
75
|
+
>
|
|
76
|
+
{children ?? <HugeiconsIcon icon={ArrowRight01Icon} strokeWidth={2} />}
|
|
77
|
+
</li>
|
|
78
|
+
);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
function BreadcrumbEllipsis({ className, ...props }: React.ComponentProps<'span'>) {
|
|
82
|
+
return (
|
|
83
|
+
<span
|
|
84
|
+
data-slot="breadcrumb-ellipsis"
|
|
85
|
+
role="presentation"
|
|
86
|
+
aria-hidden="true"
|
|
87
|
+
className={cn('flex size-4 items-center justify-center [&>svg]:size-3.5', className)}
|
|
88
|
+
{...props}
|
|
89
|
+
>
|
|
90
|
+
<HugeiconsIcon icon={MoreHorizontalCircle01Icon} strokeWidth={2} />
|
|
91
|
+
<span className="sr-only">More</span>
|
|
92
|
+
</span>
|
|
93
|
+
);
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export {
|
|
97
|
+
Breadcrumb,
|
|
98
|
+
BreadcrumbList,
|
|
99
|
+
BreadcrumbItem,
|
|
100
|
+
BreadcrumbLink,
|
|
101
|
+
BreadcrumbPage,
|
|
102
|
+
BreadcrumbSeparator,
|
|
103
|
+
BreadcrumbEllipsis,
|
|
104
|
+
};
|