@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,175 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@saena-io/ui/lib/utils';
|
|
4
|
+
import { type KeyboardEvent, type PointerEvent, useEffect, useMemo, useRef, useState } from 'react';
|
|
5
|
+
|
|
6
|
+
// A generic focal-point picker (ADR-0009): a large image preview with a draggable/clickable dot marking the
|
|
7
|
+
// "keep this in frame" point. Value is a normalized { x, y } (each 0–1, relative to the original image); unset
|
|
8
|
+
// shows the centre. Dependency-free — the consumer owns what the point means (the content ImageEditor stores it
|
|
9
|
+
// as the image field's `hotspot`).
|
|
10
|
+
//
|
|
11
|
+
// Sizing: we measure the available width + viewport and the image's natural aspect, then give the frame an
|
|
12
|
+
// EXPLICIT display box (preserving aspect, scaled to fit — and upscaling small images to a usable size). The
|
|
13
|
+
// frame's box therefore equals the displayed image exactly, so pointer coords map straight to [0,1] with no
|
|
14
|
+
// letterbox math. (A CSS-only `max-w-full` on a shrink-wrapped image is a circular size constraint that
|
|
15
|
+
// collapses to 0×0, so we compute the box instead.)
|
|
16
|
+
|
|
17
|
+
export interface FocalPoint {
|
|
18
|
+
x: number;
|
|
19
|
+
y: number;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const clamp01 = (n: number): number => Math.min(1, Math.max(0, n));
|
|
23
|
+
const CENTRE: FocalPoint = { x: 0.5, y: 0.5 };
|
|
24
|
+
const NUDGE = 0.02; // arrow-key step
|
|
25
|
+
const MIN_BOX = 160; // px floor so a measure-before-paint frame is still clickable
|
|
26
|
+
|
|
27
|
+
export function FocalPointPicker({
|
|
28
|
+
src,
|
|
29
|
+
value,
|
|
30
|
+
onChange,
|
|
31
|
+
guideRatio,
|
|
32
|
+
alt = '',
|
|
33
|
+
className,
|
|
34
|
+
}: {
|
|
35
|
+
src: string;
|
|
36
|
+
value?: FocalPoint;
|
|
37
|
+
onChange: (point: FocalPoint) => void;
|
|
38
|
+
/** Optional crop-preview overlay (w/h): outlines the area a public render at this ratio would keep, centred
|
|
39
|
+
* on the current focal point, and dims the rest. Editor guide only — it doesn't change the stored value. */
|
|
40
|
+
guideRatio?: number;
|
|
41
|
+
/** Decorative alt for the preview image (the picker is the interactive element). */
|
|
42
|
+
alt?: string;
|
|
43
|
+
className?: string;
|
|
44
|
+
}) {
|
|
45
|
+
const containerRef = useRef<HTMLDivElement>(null);
|
|
46
|
+
const frameRef = useRef<HTMLDivElement>(null);
|
|
47
|
+
const dragging = useRef(false);
|
|
48
|
+
const [natural, setNatural] = useState<{ w: number; h: number } | null>(null);
|
|
49
|
+
// Available box: container width × a fraction of the viewport height. Remeasured on resize.
|
|
50
|
+
const [avail, setAvail] = useState<{ w: number; h: number }>({ w: 0, h: 0 });
|
|
51
|
+
const point = value ?? CENTRE;
|
|
52
|
+
|
|
53
|
+
useEffect(() => {
|
|
54
|
+
const measure = () => {
|
|
55
|
+
const w = containerRef.current?.clientWidth ?? 0;
|
|
56
|
+
const h = Math.max(MIN_BOX, Math.round(window.innerHeight * 0.6));
|
|
57
|
+
setAvail({ w, h });
|
|
58
|
+
};
|
|
59
|
+
measure();
|
|
60
|
+
const ro = new ResizeObserver(measure);
|
|
61
|
+
if (containerRef.current) ro.observe(containerRef.current);
|
|
62
|
+
window.addEventListener('resize', measure);
|
|
63
|
+
return () => {
|
|
64
|
+
ro.disconnect();
|
|
65
|
+
window.removeEventListener('resize', measure);
|
|
66
|
+
};
|
|
67
|
+
}, []);
|
|
68
|
+
|
|
69
|
+
// Explicit display box: fit the natural image inside the available box, preserving aspect. `scale` may be > 1
|
|
70
|
+
// so small images grow to a usable size.
|
|
71
|
+
const box = useMemo(() => {
|
|
72
|
+
if (!natural || avail.w === 0) return null;
|
|
73
|
+
const scale = Math.min(avail.w / natural.w, avail.h / natural.h);
|
|
74
|
+
return {
|
|
75
|
+
w: Math.max(1, Math.round(natural.w * scale)),
|
|
76
|
+
h: Math.max(1, Math.round(natural.h * scale)),
|
|
77
|
+
};
|
|
78
|
+
}, [natural, avail]);
|
|
79
|
+
|
|
80
|
+
// The crop box a public render at `guideRatio` would keep, centred on the focal point + clamped — same math
|
|
81
|
+
// as the server crop, in the displayed box's px (ADR-0009). Null until the image is measured.
|
|
82
|
+
const guide = useMemo(() => {
|
|
83
|
+
if (!guideRatio || guideRatio <= 0 || !box) return null;
|
|
84
|
+
const { w: W, h: H } = box;
|
|
85
|
+
const origAR = W / H;
|
|
86
|
+
const cw = guideRatio >= origAR ? W : H * guideRatio;
|
|
87
|
+
const ch = guideRatio >= origAR ? W / guideRatio : H;
|
|
88
|
+
const left = Math.max(0, Math.min(point.x * W - cw / 2, W - cw));
|
|
89
|
+
const top = Math.max(0, Math.min(point.y * H - ch / 2, H - ch));
|
|
90
|
+
return { left, top, width: cw, height: ch };
|
|
91
|
+
}, [guideRatio, box, point.x, point.y]);
|
|
92
|
+
|
|
93
|
+
const setFromPointer = (e: PointerEvent): void => {
|
|
94
|
+
const el = frameRef.current;
|
|
95
|
+
if (!el) return;
|
|
96
|
+
const r = el.getBoundingClientRect();
|
|
97
|
+
if (r.width === 0 || r.height === 0) return;
|
|
98
|
+
onChange({
|
|
99
|
+
x: clamp01((e.clientX - r.left) / r.width),
|
|
100
|
+
y: clamp01((e.clientY - r.top) / r.height),
|
|
101
|
+
});
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const onKeyDown = (e: KeyboardEvent): void => {
|
|
105
|
+
const dx = e.key === 'ArrowLeft' ? -NUDGE : e.key === 'ArrowRight' ? NUDGE : 0;
|
|
106
|
+
const dy = e.key === 'ArrowUp' ? -NUDGE : e.key === 'ArrowDown' ? NUDGE : 0;
|
|
107
|
+
if (dx === 0 && dy === 0) return;
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
onChange({ x: clamp01(point.x + dx), y: clamp01(point.y + dy) });
|
|
110
|
+
};
|
|
111
|
+
|
|
112
|
+
return (
|
|
113
|
+
<div ref={containerRef} className={cn('w-full', className)}>
|
|
114
|
+
<div
|
|
115
|
+
ref={frameRef}
|
|
116
|
+
role="slider"
|
|
117
|
+
aria-label="Focal point"
|
|
118
|
+
// A focal point is 2D; expose x as the numeric value and the full x/y in valuetext (which AT reads in
|
|
119
|
+
// preference to valuenow). Arrow keys nudge both axes.
|
|
120
|
+
aria-valuemin={0}
|
|
121
|
+
aria-valuemax={100}
|
|
122
|
+
aria-valuenow={Math.round(point.x * 100)}
|
|
123
|
+
aria-valuetext={`x ${Math.round(point.x * 100)}%, y ${Math.round(point.y * 100)}%`}
|
|
124
|
+
tabIndex={0}
|
|
125
|
+
onKeyDown={onKeyDown}
|
|
126
|
+
onPointerDown={(e) => {
|
|
127
|
+
e.preventDefault();
|
|
128
|
+
dragging.current = true;
|
|
129
|
+
// capture so a drag that leaves the frame keeps tracking; guard — a synthetic/edge pointerId can throw.
|
|
130
|
+
try {
|
|
131
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
132
|
+
} catch {}
|
|
133
|
+
setFromPointer(e);
|
|
134
|
+
}}
|
|
135
|
+
onPointerMove={(e) => {
|
|
136
|
+
if (dragging.current) setFromPointer(e);
|
|
137
|
+
}}
|
|
138
|
+
onPointerUp={(e) => {
|
|
139
|
+
dragging.current = false;
|
|
140
|
+
try {
|
|
141
|
+
e.currentTarget.releasePointerCapture?.(e.pointerId);
|
|
142
|
+
} catch {}
|
|
143
|
+
}}
|
|
144
|
+
style={box ? { width: box.w, height: box.h } : { minHeight: MIN_BOX }}
|
|
145
|
+
className="relative mx-auto cursor-crosshair touch-none overflow-hidden rounded-md bg-muted/40 ring-1 ring-foreground/10 outline-none focus-visible:ring-2 focus-visible:ring-ring"
|
|
146
|
+
>
|
|
147
|
+
{/* The frame box equals the displayed image (computed above), so object-cover fills it without cropping.
|
|
148
|
+
draggable=false stops the native image-drag from hijacking the pointer. */}
|
|
149
|
+
<img
|
|
150
|
+
src={src}
|
|
151
|
+
alt={alt}
|
|
152
|
+
draggable={false}
|
|
153
|
+
onLoad={(e) =>
|
|
154
|
+
setNatural({ w: e.currentTarget.naturalWidth, h: e.currentTarget.naturalHeight })
|
|
155
|
+
}
|
|
156
|
+
className="block h-full w-full select-none object-cover"
|
|
157
|
+
/>
|
|
158
|
+
{guide ? (
|
|
159
|
+
<div
|
|
160
|
+
aria-hidden
|
|
161
|
+
style={{ left: guide.left, top: guide.top, width: guide.width, height: guide.height }}
|
|
162
|
+
className="pointer-events-none absolute border border-white/90 shadow-[0_0_0_9999px_rgba(0,0,0,0.5)]"
|
|
163
|
+
/>
|
|
164
|
+
) : null}
|
|
165
|
+
<span
|
|
166
|
+
aria-hidden
|
|
167
|
+
style={{ left: `${point.x * 100}%`, top: `${point.y * 100}%` }}
|
|
168
|
+
className="-translate-x-1/2 -translate-y-1/2 pointer-events-none absolute size-7 rounded-full border-2 border-white shadow-[0_0_0_2px_rgba(0,0,0,0.45)]"
|
|
169
|
+
>
|
|
170
|
+
<span className="-translate-x-1/2 -translate-y-1/2 absolute top-1/2 left-1/2 size-1.5 rounded-full bg-white shadow-[0_0_0_1px_rgba(0,0,0,0.45)]" />
|
|
171
|
+
</span>
|
|
172
|
+
</div>
|
|
173
|
+
</div>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { PreviewCard as PreviewCardPrimitive } from '@base-ui/react/preview-card';
|
|
4
|
+
|
|
5
|
+
import { cn } from '@saena-io/ui/lib/utils';
|
|
6
|
+
|
|
7
|
+
function HoverCard({ ...props }: PreviewCardPrimitive.Root.Props) {
|
|
8
|
+
return <PreviewCardPrimitive.Root data-slot="hover-card" {...props} />;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
function HoverCardTrigger({ ...props }: PreviewCardPrimitive.Trigger.Props) {
|
|
12
|
+
return <PreviewCardPrimitive.Trigger data-slot="hover-card-trigger" {...props} />;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
function HoverCardContent({
|
|
16
|
+
className,
|
|
17
|
+
side = 'bottom',
|
|
18
|
+
sideOffset = 4,
|
|
19
|
+
align = 'center',
|
|
20
|
+
alignOffset = 4,
|
|
21
|
+
...props
|
|
22
|
+
}: PreviewCardPrimitive.Popup.Props &
|
|
23
|
+
Pick<PreviewCardPrimitive.Positioner.Props, 'align' | 'alignOffset' | 'side' | 'sideOffset'>) {
|
|
24
|
+
return (
|
|
25
|
+
<PreviewCardPrimitive.Portal data-slot="hover-card-portal">
|
|
26
|
+
<PreviewCardPrimitive.Positioner
|
|
27
|
+
align={align}
|
|
28
|
+
alignOffset={alignOffset}
|
|
29
|
+
side={side}
|
|
30
|
+
sideOffset={sideOffset}
|
|
31
|
+
className="isolate z-50"
|
|
32
|
+
>
|
|
33
|
+
<PreviewCardPrimitive.Popup
|
|
34
|
+
data-slot="hover-card-content"
|
|
35
|
+
className={cn(
|
|
36
|
+
'z-50 w-72 origin-(--transform-origin) rounded-lg bg-popover p-2.5 text-xs/relaxed 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',
|
|
37
|
+
className,
|
|
38
|
+
)}
|
|
39
|
+
{...props}
|
|
40
|
+
/>
|
|
41
|
+
</PreviewCardPrimitive.Positioner>
|
|
42
|
+
</PreviewCardPrimitive.Portal>
|
|
43
|
+
);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
export { HoverCard, HoverCardTrigger, HoverCardContent };
|
|
@@ -0,0 +1,149 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import { type VariantProps, cva } from 'class-variance-authority';
|
|
4
|
+
import type * as React from 'react';
|
|
5
|
+
|
|
6
|
+
import { Button } from '@saena-io/ui/components/button';
|
|
7
|
+
import { Input } from '@saena-io/ui/components/input';
|
|
8
|
+
import { Textarea } from '@saena-io/ui/components/textarea';
|
|
9
|
+
import { cn } from '@saena-io/ui/lib/utils';
|
|
10
|
+
|
|
11
|
+
function InputGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
|
12
|
+
return (
|
|
13
|
+
<div
|
|
14
|
+
data-slot="input-group"
|
|
15
|
+
role="group"
|
|
16
|
+
className={cn(
|
|
17
|
+
'group/input-group relative flex h-7 w-full min-w-0 items-center rounded-md border border-input bg-input/20 transition-colors outline-none in-data-[slot=combobox-content]:focus-within:border-inherit in-data-[slot=combobox-content]:focus-within:ring-0 has-data-[align=block-end]:rounded-md has-data-[align=block-start]:rounded-md has-[[data-slot=input-group-control]:focus-visible]:border-ring has-[[data-slot=input-group-control]:focus-visible]:ring-2 has-[[data-slot=input-group-control]:focus-visible]:ring-ring/30 has-[[data-slot][aria-invalid=true]]:border-destructive has-[[data-slot][aria-invalid=true]]:ring-2 has-[[data-slot][aria-invalid=true]]:ring-destructive/20 has-[textarea]:rounded-md has-[>[data-align=block-end]]:h-auto has-[>[data-align=block-end]]:flex-col has-[>[data-align=block-start]]:h-auto has-[>[data-align=block-start]]:flex-col has-[>textarea]:h-auto dark:bg-input/30 dark:has-[[data-slot][aria-invalid=true]]:ring-destructive/40 has-[>[data-align=block-end]]:[&>input]:pt-3 has-[>[data-align=block-start]]:[&>input]:pb-3 has-[>[data-align=inline-end]]:[&>input]:pr-1.5 has-[>[data-align=inline-start]]:[&>input]:pl-1.5',
|
|
18
|
+
className,
|
|
19
|
+
)}
|
|
20
|
+
{...props}
|
|
21
|
+
/>
|
|
22
|
+
);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
const inputGroupAddonVariants = cva(
|
|
26
|
+
"flex h-auto cursor-text items-center justify-center gap-1 py-2 text-xs/relaxed font-medium text-muted-foreground select-none group-data-[disabled=true]/input-group:opacity-50 **:data-[slot=kbd]:rounded-[calc(var(--radius-sm)-2px)] **:data-[slot=kbd]:bg-muted-foreground/10 **:data-[slot=kbd]:px-1 **:data-[slot=kbd]:text-[0.625rem] [&>svg:not([class*='size-'])]:size-3.5",
|
|
27
|
+
{
|
|
28
|
+
variants: {
|
|
29
|
+
align: {
|
|
30
|
+
'inline-start': 'order-first pl-2 has-[>button]:ml-[-0.275rem] has-[>kbd]:ml-[-0.275rem]',
|
|
31
|
+
'inline-end': 'order-last pr-2 has-[>button]:mr-[-0.275rem] has-[>kbd]:mr-[-0.275rem]',
|
|
32
|
+
'block-start':
|
|
33
|
+
'order-first w-full justify-start px-2 pt-2 group-has-[>input]/input-group:pt-2 [.border-b]:pb-2',
|
|
34
|
+
'block-end':
|
|
35
|
+
'order-last w-full justify-start px-2 pb-2 group-has-[>input]/input-group:pb-2 [.border-t]:pt-2',
|
|
36
|
+
},
|
|
37
|
+
},
|
|
38
|
+
defaultVariants: {
|
|
39
|
+
align: 'inline-start',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
function InputGroupAddon({
|
|
45
|
+
className,
|
|
46
|
+
align = 'inline-start',
|
|
47
|
+
...props
|
|
48
|
+
}: React.ComponentProps<'div'> & VariantProps<typeof inputGroupAddonVariants>) {
|
|
49
|
+
return (
|
|
50
|
+
<div
|
|
51
|
+
role="group"
|
|
52
|
+
data-slot="input-group-addon"
|
|
53
|
+
data-align={align}
|
|
54
|
+
className={cn(inputGroupAddonVariants({ align }), className)}
|
|
55
|
+
onClick={(e) => {
|
|
56
|
+
if ((e.target as HTMLElement).closest('button')) {
|
|
57
|
+
return;
|
|
58
|
+
}
|
|
59
|
+
e.currentTarget.parentElement?.querySelector('input')?.focus();
|
|
60
|
+
}}
|
|
61
|
+
{...props}
|
|
62
|
+
/>
|
|
63
|
+
);
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
const inputGroupButtonVariants = cva(
|
|
67
|
+
'flex items-center gap-2 rounded-md text-xs/relaxed shadow-none',
|
|
68
|
+
{
|
|
69
|
+
variants: {
|
|
70
|
+
size: {
|
|
71
|
+
xs: "h-5 gap-1 rounded-[calc(var(--radius-sm)-2px)] px-1 [&>svg:not([class*='size-'])]:size-3",
|
|
72
|
+
sm: 'gap-1',
|
|
73
|
+
'icon-xs': 'size-6 p-0 has-[>svg]:p-0',
|
|
74
|
+
'icon-sm': 'size-7 p-0 has-[>svg]:p-0',
|
|
75
|
+
},
|
|
76
|
+
},
|
|
77
|
+
defaultVariants: {
|
|
78
|
+
size: 'xs',
|
|
79
|
+
},
|
|
80
|
+
},
|
|
81
|
+
);
|
|
82
|
+
|
|
83
|
+
function InputGroupButton({
|
|
84
|
+
className,
|
|
85
|
+
type = 'button',
|
|
86
|
+
variant = 'ghost',
|
|
87
|
+
size = 'xs',
|
|
88
|
+
...props
|
|
89
|
+
}: Omit<React.ComponentProps<typeof Button>, 'size' | 'type'> &
|
|
90
|
+
VariantProps<typeof inputGroupButtonVariants> & {
|
|
91
|
+
type?: 'button' | 'submit' | 'reset';
|
|
92
|
+
}) {
|
|
93
|
+
return (
|
|
94
|
+
<Button
|
|
95
|
+
type={type}
|
|
96
|
+
data-size={size}
|
|
97
|
+
variant={variant}
|
|
98
|
+
className={cn(inputGroupButtonVariants({ size }), className)}
|
|
99
|
+
{...props}
|
|
100
|
+
/>
|
|
101
|
+
);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
function InputGroupText({ className, ...props }: React.ComponentProps<'span'>) {
|
|
105
|
+
return (
|
|
106
|
+
<span
|
|
107
|
+
className={cn(
|
|
108
|
+
"flex items-center gap-2 text-xs/relaxed text-muted-foreground [&_svg]:pointer-events-none [&_svg:not([class*='size-'])]:size-4",
|
|
109
|
+
className,
|
|
110
|
+
)}
|
|
111
|
+
{...props}
|
|
112
|
+
/>
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
function InputGroupInput({ className, ...props }: React.ComponentProps<'input'>) {
|
|
117
|
+
return (
|
|
118
|
+
<Input
|
|
119
|
+
data-slot="input-group-control"
|
|
120
|
+
className={cn(
|
|
121
|
+
'flex-1 rounded-none border-0 bg-transparent shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent',
|
|
122
|
+
className,
|
|
123
|
+
)}
|
|
124
|
+
{...props}
|
|
125
|
+
/>
|
|
126
|
+
);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
function InputGroupTextarea({ className, ...props }: React.ComponentProps<'textarea'>) {
|
|
130
|
+
return (
|
|
131
|
+
<Textarea
|
|
132
|
+
data-slot="input-group-control"
|
|
133
|
+
className={cn(
|
|
134
|
+
'flex-1 resize-none rounded-none border-0 bg-transparent py-2 shadow-none ring-0 focus-visible:ring-0 aria-invalid:ring-0 dark:bg-transparent',
|
|
135
|
+
className,
|
|
136
|
+
)}
|
|
137
|
+
{...props}
|
|
138
|
+
/>
|
|
139
|
+
);
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export {
|
|
143
|
+
InputGroup,
|
|
144
|
+
InputGroupAddon,
|
|
145
|
+
InputGroupButton,
|
|
146
|
+
InputGroupText,
|
|
147
|
+
InputGroupInput,
|
|
148
|
+
InputGroupTextarea,
|
|
149
|
+
};
|
|
@@ -0,0 +1,85 @@
|
|
|
1
|
+
import { OTPInput, OTPInputContext } from 'input-otp';
|
|
2
|
+
import * as React from 'react';
|
|
3
|
+
|
|
4
|
+
import { MinusSignIcon } from '@hugeicons/core-free-icons';
|
|
5
|
+
import { HugeiconsIcon } from '@hugeicons/react';
|
|
6
|
+
import { cn } from '@saena-io/ui/lib/utils';
|
|
7
|
+
|
|
8
|
+
function InputOTP({
|
|
9
|
+
className,
|
|
10
|
+
containerClassName,
|
|
11
|
+
...props
|
|
12
|
+
}: React.ComponentProps<typeof OTPInput> & {
|
|
13
|
+
containerClassName?: string;
|
|
14
|
+
}) {
|
|
15
|
+
return (
|
|
16
|
+
<OTPInput
|
|
17
|
+
data-slot="input-otp"
|
|
18
|
+
containerClassName={cn(
|
|
19
|
+
'cn-input-otp flex items-center has-disabled:opacity-50',
|
|
20
|
+
containerClassName,
|
|
21
|
+
)}
|
|
22
|
+
spellCheck={false}
|
|
23
|
+
className={cn('disabled:cursor-not-allowed', className)}
|
|
24
|
+
{...props}
|
|
25
|
+
/>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function InputOTPGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
|
30
|
+
return (
|
|
31
|
+
<div
|
|
32
|
+
data-slot="input-otp-group"
|
|
33
|
+
className={cn(
|
|
34
|
+
'flex items-center rounded-md has-aria-invalid:border-destructive has-aria-invalid:ring-2 has-aria-invalid:ring-destructive/20 dark:has-aria-invalid:ring-destructive/40',
|
|
35
|
+
className,
|
|
36
|
+
)}
|
|
37
|
+
{...props}
|
|
38
|
+
/>
|
|
39
|
+
);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function InputOTPSlot({
|
|
43
|
+
index,
|
|
44
|
+
className,
|
|
45
|
+
...props
|
|
46
|
+
}: React.ComponentProps<'div'> & {
|
|
47
|
+
index: number;
|
|
48
|
+
}) {
|
|
49
|
+
const inputOTPContext = React.useContext(OTPInputContext);
|
|
50
|
+
const { char, hasFakeCaret, isActive } = inputOTPContext?.slots[index] ?? {};
|
|
51
|
+
|
|
52
|
+
return (
|
|
53
|
+
<div
|
|
54
|
+
data-slot="input-otp-slot"
|
|
55
|
+
data-active={isActive}
|
|
56
|
+
className={cn(
|
|
57
|
+
'relative flex size-7 items-center justify-center border-y border-r border-input bg-input/20 text-xs/relaxed transition-all outline-none first:rounded-l-md first:border-l last:rounded-r-md aria-invalid:border-destructive data-[active=true]:z-10 data-[active=true]:border-ring data-[active=true]:ring-2 data-[active=true]:ring-ring/30 data-[active=true]:aria-invalid:border-destructive data-[active=true]:aria-invalid:ring-destructive/20 dark:bg-input/30 dark:data-[active=true]:aria-invalid:ring-destructive/40',
|
|
58
|
+
className,
|
|
59
|
+
)}
|
|
60
|
+
{...props}
|
|
61
|
+
>
|
|
62
|
+
{char}
|
|
63
|
+
{hasFakeCaret && (
|
|
64
|
+
<div className="pointer-events-none absolute inset-0 flex items-center justify-center">
|
|
65
|
+
<div className="h-4 w-px animate-caret-blink bg-foreground duration-1000" />
|
|
66
|
+
</div>
|
|
67
|
+
)}
|
|
68
|
+
</div>
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
function InputOTPSeparator({ ...props }: React.ComponentProps<'div'>) {
|
|
73
|
+
return (
|
|
74
|
+
<div
|
|
75
|
+
data-slot="input-otp-separator"
|
|
76
|
+
className="flex items-center [&_svg:not([class*='size-'])]:size-4"
|
|
77
|
+
role="separator"
|
|
78
|
+
{...props}
|
|
79
|
+
>
|
|
80
|
+
<HugeiconsIcon icon={MinusSignIcon} strokeWidth={2} />
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
export { InputOTP, InputOTPGroup, InputOTPSlot, InputOTPSeparator };
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
import { Input as InputPrimitive } from '@base-ui/react/input';
|
|
2
|
+
import type * as React from 'react';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@saena-io/ui/lib/utils';
|
|
5
|
+
|
|
6
|
+
function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
|
|
7
|
+
return (
|
|
8
|
+
<InputPrimitive
|
|
9
|
+
type={type}
|
|
10
|
+
data-slot="input"
|
|
11
|
+
className={cn(
|
|
12
|
+
'h-7 w-full min-w-0 rounded-md border border-input bg-input/20 px-2 py-0.5 text-sm transition-colors outline-none file:inline-flex file:h-6 file:border-0 file:bg-transparent file:text-xs/relaxed file:font-medium file:text-foreground placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-2 focus-visible:ring-ring/30 disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/20 md:text-xs/relaxed dark:bg-input/30 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40',
|
|
13
|
+
className,
|
|
14
|
+
)}
|
|
15
|
+
{...props}
|
|
16
|
+
/>
|
|
17
|
+
);
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export { Input };
|
|
@@ -0,0 +1,188 @@
|
|
|
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
|
+
import type * as React from 'react';
|
|
5
|
+
|
|
6
|
+
import { Separator } from '@saena-io/ui/components/separator';
|
|
7
|
+
import { cn } from '@saena-io/ui/lib/utils';
|
|
8
|
+
|
|
9
|
+
function ItemGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
role="list"
|
|
13
|
+
data-slot="item-group"
|
|
14
|
+
className={cn(
|
|
15
|
+
'group/item-group flex w-full flex-col gap-4 has-data-[size=sm]:gap-2.5 has-data-[size=xs]:gap-2',
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function ItemSeparator({ className, ...props }: React.ComponentProps<typeof Separator>) {
|
|
24
|
+
return (
|
|
25
|
+
<Separator
|
|
26
|
+
data-slot="item-separator"
|
|
27
|
+
orientation="horizontal"
|
|
28
|
+
className={cn('my-2', className)}
|
|
29
|
+
{...props}
|
|
30
|
+
/>
|
|
31
|
+
);
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
const itemVariants = cva(
|
|
35
|
+
'group/item flex w-full flex-wrap items-center rounded-md border text-xs/relaxed transition-colors duration-100 outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 [a]:transition-colors [a]:hover:bg-muted',
|
|
36
|
+
{
|
|
37
|
+
variants: {
|
|
38
|
+
variant: {
|
|
39
|
+
default: 'border-transparent',
|
|
40
|
+
outline: 'border-border',
|
|
41
|
+
muted: 'border-transparent bg-muted/50',
|
|
42
|
+
},
|
|
43
|
+
size: {
|
|
44
|
+
default: 'gap-2.5 px-3 py-2.5',
|
|
45
|
+
sm: 'gap-2.5 px-3 py-2.5',
|
|
46
|
+
xs: 'gap-2.5 px-2.5 py-2 in-data-[slot=dropdown-menu-content]:p-0',
|
|
47
|
+
},
|
|
48
|
+
},
|
|
49
|
+
defaultVariants: {
|
|
50
|
+
variant: 'default',
|
|
51
|
+
size: 'default',
|
|
52
|
+
},
|
|
53
|
+
},
|
|
54
|
+
);
|
|
55
|
+
|
|
56
|
+
function Item({
|
|
57
|
+
className,
|
|
58
|
+
variant = 'default',
|
|
59
|
+
size = 'default',
|
|
60
|
+
render,
|
|
61
|
+
...props
|
|
62
|
+
}: useRender.ComponentProps<'div'> & VariantProps<typeof itemVariants>) {
|
|
63
|
+
return useRender({
|
|
64
|
+
defaultTagName: 'div',
|
|
65
|
+
props: mergeProps<'div'>(
|
|
66
|
+
{
|
|
67
|
+
className: cn(itemVariants({ variant, size, className })),
|
|
68
|
+
},
|
|
69
|
+
props,
|
|
70
|
+
),
|
|
71
|
+
render,
|
|
72
|
+
state: {
|
|
73
|
+
slot: 'item',
|
|
74
|
+
variant,
|
|
75
|
+
size,
|
|
76
|
+
},
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const itemMediaVariants = cva(
|
|
81
|
+
'flex shrink-0 items-center justify-center gap-2 group-has-data-[slot=item-description]/item:translate-y-0.5 group-has-data-[slot=item-description]/item:self-start [&_svg]:pointer-events-none',
|
|
82
|
+
{
|
|
83
|
+
variants: {
|
|
84
|
+
variant: {
|
|
85
|
+
default: 'bg-transparent',
|
|
86
|
+
icon: "[&_svg:not([class*='size-'])]:size-4",
|
|
87
|
+
image:
|
|
88
|
+
'size-8 overflow-hidden rounded-sm group-data-[size=sm]/item:size-8 group-data-[size=xs]/item:size-6 [&_img]:size-full [&_img]:object-cover',
|
|
89
|
+
},
|
|
90
|
+
},
|
|
91
|
+
defaultVariants: {
|
|
92
|
+
variant: 'default',
|
|
93
|
+
},
|
|
94
|
+
},
|
|
95
|
+
);
|
|
96
|
+
|
|
97
|
+
function ItemMedia({
|
|
98
|
+
className,
|
|
99
|
+
variant = 'default',
|
|
100
|
+
...props
|
|
101
|
+
}: React.ComponentProps<'div'> & VariantProps<typeof itemMediaVariants>) {
|
|
102
|
+
return (
|
|
103
|
+
<div
|
|
104
|
+
data-slot="item-media"
|
|
105
|
+
data-variant={variant}
|
|
106
|
+
className={cn(itemMediaVariants({ variant, className }))}
|
|
107
|
+
{...props}
|
|
108
|
+
/>
|
|
109
|
+
);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
function ItemContent({ className, ...props }: React.ComponentProps<'div'>) {
|
|
113
|
+
return (
|
|
114
|
+
<div
|
|
115
|
+
data-slot="item-content"
|
|
116
|
+
className={cn(
|
|
117
|
+
'flex flex-1 flex-col gap-1 group-data-[size=xs]/item:gap-0.5 [&+[data-slot=item-content]]:flex-none',
|
|
118
|
+
className,
|
|
119
|
+
)}
|
|
120
|
+
{...props}
|
|
121
|
+
/>
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function ItemTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
|
126
|
+
return (
|
|
127
|
+
<div
|
|
128
|
+
data-slot="item-title"
|
|
129
|
+
className={cn(
|
|
130
|
+
'line-clamp-1 flex w-fit items-center gap-2 text-xs/relaxed leading-snug font-medium underline-offset-4',
|
|
131
|
+
className,
|
|
132
|
+
)}
|
|
133
|
+
{...props}
|
|
134
|
+
/>
|
|
135
|
+
);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
function ItemDescription({ className, ...props }: React.ComponentProps<'p'>) {
|
|
139
|
+
return (
|
|
140
|
+
<p
|
|
141
|
+
data-slot="item-description"
|
|
142
|
+
className={cn(
|
|
143
|
+
'line-clamp-2 text-left text-xs/relaxed font-normal text-muted-foreground [&>a]:underline [&>a]:underline-offset-4 [&>a:hover]:text-primary',
|
|
144
|
+
className,
|
|
145
|
+
)}
|
|
146
|
+
{...props}
|
|
147
|
+
/>
|
|
148
|
+
);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
function ItemActions({ className, ...props }: React.ComponentProps<'div'>) {
|
|
152
|
+
return (
|
|
153
|
+
<div data-slot="item-actions" className={cn('flex items-center gap-2', className)} {...props} />
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
function ItemHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|
158
|
+
return (
|
|
159
|
+
<div
|
|
160
|
+
data-slot="item-header"
|
|
161
|
+
className={cn('flex basis-full items-center justify-between gap-2', className)}
|
|
162
|
+
{...props}
|
|
163
|
+
/>
|
|
164
|
+
);
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
function ItemFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
|
168
|
+
return (
|
|
169
|
+
<div
|
|
170
|
+
data-slot="item-footer"
|
|
171
|
+
className={cn('flex basis-full items-center justify-between gap-2', className)}
|
|
172
|
+
{...props}
|
|
173
|
+
/>
|
|
174
|
+
);
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
export {
|
|
178
|
+
Item,
|
|
179
|
+
ItemMedia,
|
|
180
|
+
ItemContent,
|
|
181
|
+
ItemActions,
|
|
182
|
+
ItemGroup,
|
|
183
|
+
ItemSeparator,
|
|
184
|
+
ItemTitle,
|
|
185
|
+
ItemDescription,
|
|
186
|
+
ItemHeader,
|
|
187
|
+
ItemFooter,
|
|
188
|
+
};
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { cn } from '@saena-io/ui/lib/utils';
|
|
2
|
+
|
|
3
|
+
function Kbd({ className, ...props }: React.ComponentProps<'kbd'>) {
|
|
4
|
+
return (
|
|
5
|
+
<kbd
|
|
6
|
+
data-slot="kbd"
|
|
7
|
+
className={cn(
|
|
8
|
+
"pointer-events-none inline-flex h-5 w-fit min-w-5 items-center justify-center gap-1 rounded-xs bg-muted px-1 font-sans text-[0.625rem] font-medium text-muted-foreground select-none in-data-[slot=tooltip-content]:bg-background/20 in-data-[slot=tooltip-content]:text-background dark:in-data-[slot=tooltip-content]:bg-background/10 [&_svg:not([class*='size-'])]:size-3",
|
|
9
|
+
className,
|
|
10
|
+
)}
|
|
11
|
+
{...props}
|
|
12
|
+
/>
|
|
13
|
+
);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
function KbdGroup({ className, ...props }: React.ComponentProps<'div'>) {
|
|
17
|
+
return (
|
|
18
|
+
<kbd
|
|
19
|
+
data-slot="kbd-group"
|
|
20
|
+
className={cn('inline-flex items-center gap-1', className)}
|
|
21
|
+
{...props}
|
|
22
|
+
/>
|
|
23
|
+
);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export { Kbd, KbdGroup };
|