@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.
Files changed (100) hide show
  1. package/dist/index.js +9 -9
  2. package/package.json +1 -1
  3. package/template/base/package.json +44 -2
  4. package/template/base/scripts/ui-update.ts +83 -0
  5. package/template/base/src/components/ui/accordion.tsx +75 -0
  6. package/template/base/src/components/ui/alert-dialog.tsx +162 -0
  7. package/template/base/src/components/ui/alert.tsx +73 -0
  8. package/template/base/src/components/ui/app-sidebar.tsx +183 -0
  9. package/template/base/src/components/ui/aspect-ratio.tsx +22 -0
  10. package/template/base/src/components/ui/asset-input.tsx +211 -0
  11. package/template/base/src/components/ui/avatar.tsx +91 -0
  12. package/template/base/src/components/ui/badge.tsx +50 -0
  13. package/template/base/src/components/ui/breadcrumb.tsx +104 -0
  14. package/template/base/src/components/ui/button-group.tsx +78 -0
  15. package/template/base/src/components/ui/button.tsx +56 -0
  16. package/template/base/src/components/ui/calendar.tsx +205 -0
  17. package/template/base/src/components/ui/card.tsx +85 -0
  18. package/template/base/src/components/ui/carousel.tsx +232 -0
  19. package/template/base/src/components/ui/chart.tsx +337 -0
  20. package/template/base/src/components/ui/checkbox.tsx +29 -0
  21. package/template/base/src/components/ui/collapsible.tsx +15 -0
  22. package/template/base/src/components/ui/combobox.tsx +276 -0
  23. package/template/base/src/components/ui/command.tsx +190 -0
  24. package/template/base/src/components/ui/context-menu.tsx +243 -0
  25. package/template/base/src/components/ui/dialog.tsx +134 -0
  26. package/template/base/src/components/ui/direction.tsx +4 -0
  27. package/template/base/src/components/ui/drawer.tsx +120 -0
  28. package/template/base/src/components/ui/dropdown-menu.tsx +254 -0
  29. package/template/base/src/components/ui/empty.tsx +94 -0
  30. package/template/base/src/components/ui/field.tsx +222 -0
  31. package/template/base/src/components/ui/focal-point-picker.tsx +175 -0
  32. package/template/base/src/components/ui/hover-card.tsx +46 -0
  33. package/template/base/src/components/ui/input-group.tsx +149 -0
  34. package/template/base/src/components/ui/input-otp.tsx +85 -0
  35. package/template/base/src/components/ui/input.tsx +20 -0
  36. package/template/base/src/components/ui/item.tsx +188 -0
  37. package/template/base/src/components/ui/kbd.tsx +26 -0
  38. package/template/base/src/components/ui/label.tsx +20 -0
  39. package/template/base/src/components/ui/menubar.tsx +268 -0
  40. package/template/base/src/components/ui/native-select.tsx +58 -0
  41. package/template/base/src/components/ui/nav-main.tsx +70 -0
  42. package/template/base/src/components/ui/nav-projects.tsx +97 -0
  43. package/template/base/src/components/ui/nav-secondary.tsx +37 -0
  44. package/template/base/src/components/ui/nav-user.tsx +108 -0
  45. package/template/base/src/components/ui/navigation-menu.tsx +164 -0
  46. package/template/base/src/components/ui/pagination.tsx +123 -0
  47. package/template/base/src/components/ui/popover.tsx +80 -0
  48. package/template/base/src/components/ui/progress.tsx +66 -0
  49. package/template/base/src/components/ui/radio-group.tsx +36 -0
  50. package/template/base/src/components/ui/resizable.tsx +42 -0
  51. package/template/base/src/components/ui/rich-text/ai-chat-editor.tsx +20 -0
  52. package/template/base/src/components/ui/rich-text/ai-command.tsx +90 -0
  53. package/template/base/src/components/ui/rich-text/ai-copilot.tsx +67 -0
  54. package/template/base/src/components/ui/rich-text/ai-menu.tsx +456 -0
  55. package/template/base/src/components/ui/rich-text/ai-node.tsx +42 -0
  56. package/template/base/src/components/ui/rich-text/ai-toolbar-button.tsx +29 -0
  57. package/template/base/src/components/ui/rich-text/block-draggable.tsx +187 -0
  58. package/template/base/src/components/ui/rich-text/block-selection.tsx +17 -0
  59. package/template/base/src/components/ui/rich-text/code-block-node.tsx +204 -0
  60. package/template/base/src/components/ui/rich-text/codec.ts +63 -0
  61. package/template/base/src/components/ui/rich-text/extension.ts +53 -0
  62. package/template/base/src/components/ui/rich-text/ghost-text.tsx +23 -0
  63. package/template/base/src/components/ui/rich-text/import-export-toolbar.tsx +103 -0
  64. package/template/base/src/components/ui/rich-text/link.tsx +18 -0
  65. package/template/base/src/components/ui/rich-text/list-node.tsx +65 -0
  66. package/template/base/src/components/ui/rich-text/nodes.tsx +44 -0
  67. package/template/base/src/components/ui/rich-text/plugins.ts +233 -0
  68. package/template/base/src/components/ui/rich-text/rich-text-editor.tsx +82 -0
  69. package/template/base/src/components/ui/rich-text/static.tsx +117 -0
  70. package/template/base/src/components/ui/rich-text/table-node.tsx +934 -0
  71. package/template/base/src/components/ui/rich-text/table-toolbar.tsx +232 -0
  72. package/template/base/src/components/ui/rich-text/toggle-node.tsx +36 -0
  73. package/template/base/src/components/ui/rich-text/toolbar-slots.ts +41 -0
  74. package/template/base/src/components/ui/rich-text/toolbar.tsx +668 -0
  75. package/template/base/src/components/ui/rich-text/use-ai-chat.ts +35 -0
  76. package/template/base/src/components/ui/rich-text/variable-type.ts +4 -0
  77. package/template/base/src/components/ui/rich-text/variable.tsx +97 -0
  78. package/template/base/src/components/ui/scroll-area.tsx +49 -0
  79. package/template/base/src/components/ui/select.tsx +202 -0
  80. package/template/base/src/components/ui/separator.tsx +19 -0
  81. package/template/base/src/components/ui/sheet.tsx +126 -0
  82. package/template/base/src/components/ui/sidebar.tsx +695 -0
  83. package/template/base/src/components/ui/skeleton.tsx +13 -0
  84. package/template/base/src/components/ui/slider.tsx +52 -0
  85. package/template/base/src/components/ui/sonner.tsx +50 -0
  86. package/template/base/src/components/ui/spinner.tsx +18 -0
  87. package/template/base/src/components/ui/switch.tsx +30 -0
  88. package/template/base/src/components/ui/table.tsx +89 -0
  89. package/template/base/src/components/ui/tabs.tsx +73 -0
  90. package/template/base/src/components/ui/textarea.tsx +18 -0
  91. package/template/base/src/components/ui/toggle-group.tsx +85 -0
  92. package/template/base/src/components/ui/toggle.tsx +45 -0
  93. package/template/base/src/components/ui/toolbar.tsx +451 -0
  94. package/template/base/src/components/ui/tooltip.tsx +52 -0
  95. package/template/base/src/hooks/use-mobile.ts +19 -0
  96. package/template/base/src/lib/utils.ts +6 -0
  97. package/template/base/src/routes/__root.tsx +1 -1
  98. package/template/base/src/server/auth.ts +2 -2
  99. package/template/base/src/styles/globals.css +230 -0
  100. 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
+ };