@open-slide/core 0.0.1
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/build-0xQdMJb7.js +14 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +58 -0
- package/dist/config-Dk8ASJ8X.js +324 -0
- package/dist/dev-BN2k5C-N.js +14 -0
- package/dist/index.d.ts +17 -0
- package/dist/index.js +6 -0
- package/dist/preview-B-xUqFKf.js +12 -0
- package/dist/vite/index.d.ts +18 -0
- package/dist/vite/index.js +3 -0
- package/package.json +67 -0
- package/src/app/App.tsx +14 -0
- package/src/app/components/Player.tsx +61 -0
- package/src/app/components/SlideCanvas.tsx +70 -0
- package/src/app/components/ThumbnailRail.tsx +57 -0
- package/src/app/components/inspector/CommentPopover.tsx +102 -0
- package/src/app/components/inspector/CommentWidget.tsx +63 -0
- package/src/app/components/inspector/InspectOverlay.tsx +94 -0
- package/src/app/components/inspector/InspectorProvider.tsx +75 -0
- package/src/app/components/ui/badge.tsx +45 -0
- package/src/app/components/ui/button.tsx +67 -0
- package/src/app/components/ui/card.tsx +92 -0
- package/src/app/components/ui/scroll-area.tsx +53 -0
- package/src/app/components/ui/separator.tsx +28 -0
- package/src/app/index.html +12 -0
- package/src/app/lib/decks.ts +8 -0
- package/src/app/lib/inspector/fiber.ts +39 -0
- package/src/app/lib/inspector/useComments.ts +74 -0
- package/src/app/lib/sdk.ts +16 -0
- package/src/app/lib/utils.ts +6 -0
- package/src/app/main.tsx +10 -0
- package/src/app/routes/Deck.tsx +185 -0
- package/src/app/routes/Home.tsx +98 -0
- package/src/app/styles.css +130 -0
- package/src/app/virtual.d.ts +14 -0
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
3
|
+
import type { SlidePage } from '../lib/sdk';
|
|
4
|
+
import { SlideCanvas } from './SlideCanvas';
|
|
5
|
+
import { CANVAS_WIDTH, CANVAS_HEIGHT } from '../lib/sdk';
|
|
6
|
+
|
|
7
|
+
type Props = {
|
|
8
|
+
pages: SlidePage[];
|
|
9
|
+
current: number;
|
|
10
|
+
onSelect: (index: number) => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
const THUMB_WIDTH = 200;
|
|
14
|
+
const THUMB_SCALE = THUMB_WIDTH / CANVAS_WIDTH;
|
|
15
|
+
const THUMB_HEIGHT = CANVAS_HEIGHT * THUMB_SCALE;
|
|
16
|
+
|
|
17
|
+
export function ThumbnailRail({ pages, current, onSelect }: Props) {
|
|
18
|
+
return (
|
|
19
|
+
<ScrollArea className="h-full border-r bg-card">
|
|
20
|
+
<aside className="flex flex-col gap-2.5 p-3">
|
|
21
|
+
{pages.map((Page, i) => {
|
|
22
|
+
const active = i === current;
|
|
23
|
+
return (
|
|
24
|
+
<button
|
|
25
|
+
key={i}
|
|
26
|
+
onClick={() => onSelect(i)}
|
|
27
|
+
aria-label={`Go to slide ${i + 1}`}
|
|
28
|
+
aria-current={active ? 'true' : undefined}
|
|
29
|
+
className={cn(
|
|
30
|
+
'flex items-center gap-2.5 rounded-lg border-2 border-transparent p-1.5 text-left transition-colors',
|
|
31
|
+
'hover:bg-muted',
|
|
32
|
+
active && 'border-primary bg-primary/5',
|
|
33
|
+
)}
|
|
34
|
+
>
|
|
35
|
+
<span
|
|
36
|
+
className={cn(
|
|
37
|
+
'w-5 shrink-0 text-right text-xs tabular-nums text-muted-foreground',
|
|
38
|
+
active && 'font-semibold text-primary',
|
|
39
|
+
)}
|
|
40
|
+
>
|
|
41
|
+
{i + 1}
|
|
42
|
+
</span>
|
|
43
|
+
<div
|
|
44
|
+
className="relative shrink-0 overflow-hidden rounded border bg-white shadow-sm"
|
|
45
|
+
style={{ width: THUMB_WIDTH, height: THUMB_HEIGHT }}
|
|
46
|
+
>
|
|
47
|
+
<SlideCanvas scale={THUMB_SCALE} center={false} flat>
|
|
48
|
+
<Page />
|
|
49
|
+
</SlideCanvas>
|
|
50
|
+
</div>
|
|
51
|
+
</button>
|
|
52
|
+
);
|
|
53
|
+
})}
|
|
54
|
+
</aside>
|
|
55
|
+
</ScrollArea>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { createPortal } from 'react-dom';
|
|
3
|
+
import { useInspector } from './InspectorProvider';
|
|
4
|
+
|
|
5
|
+
const POPOVER_W = 320;
|
|
6
|
+
const POPOVER_H = 180;
|
|
7
|
+
|
|
8
|
+
export function CommentPopover() {
|
|
9
|
+
const { pending, setPending, add } = useInspector();
|
|
10
|
+
const [text, setText] = useState('');
|
|
11
|
+
const [submitting, setSubmitting] = useState(false);
|
|
12
|
+
const [error, setError] = useState<string | null>(null);
|
|
13
|
+
const taRef = useRef<HTMLTextAreaElement>(null);
|
|
14
|
+
|
|
15
|
+
useEffect(() => {
|
|
16
|
+
taRef.current?.focus();
|
|
17
|
+
}, []);
|
|
18
|
+
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const onKey = (e: KeyboardEvent) => {
|
|
21
|
+
if (e.key === 'Escape') setPending(null);
|
|
22
|
+
};
|
|
23
|
+
window.addEventListener('keydown', onKey);
|
|
24
|
+
return () => window.removeEventListener('keydown', onKey);
|
|
25
|
+
}, [setPending]);
|
|
26
|
+
|
|
27
|
+
if (!pending) return null;
|
|
28
|
+
|
|
29
|
+
const left = clamp(pending.clickX + 12, 8, window.innerWidth - POPOVER_W - 8);
|
|
30
|
+
const top = clamp(pending.clickY + 12, 8, window.innerHeight - POPOVER_H - 8);
|
|
31
|
+
|
|
32
|
+
const onSubmit = async () => {
|
|
33
|
+
const trimmed = text.trim();
|
|
34
|
+
if (!trimmed) return;
|
|
35
|
+
setSubmitting(true);
|
|
36
|
+
try {
|
|
37
|
+
await add(pending.line, pending.column, trimmed);
|
|
38
|
+
setPending(null);
|
|
39
|
+
} catch (e) {
|
|
40
|
+
setError(String((e as Error).message ?? e));
|
|
41
|
+
setSubmitting(false);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
return createPortal(
|
|
46
|
+
<div
|
|
47
|
+
data-inspector-ui
|
|
48
|
+
className="fixed z-50 rounded-md border bg-card p-3 shadow-xl"
|
|
49
|
+
style={{ left, top, width: POPOVER_W }}
|
|
50
|
+
>
|
|
51
|
+
<div className="mb-2 flex items-center justify-between">
|
|
52
|
+
<span className="text-xs font-medium text-muted-foreground">
|
|
53
|
+
Line {pending.line} · Comment
|
|
54
|
+
</span>
|
|
55
|
+
<button
|
|
56
|
+
type="button"
|
|
57
|
+
className="text-xs text-muted-foreground hover:text-foreground"
|
|
58
|
+
onClick={() => setPending(null)}
|
|
59
|
+
>
|
|
60
|
+
✕
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
<textarea
|
|
64
|
+
ref={taRef}
|
|
65
|
+
value={text}
|
|
66
|
+
onChange={(e) => setText(e.target.value)}
|
|
67
|
+
placeholder="Describe the change…"
|
|
68
|
+
className="h-20 w-full resize-none rounded border bg-background p-2 text-sm outline-none focus:ring-2 focus:ring-primary/40"
|
|
69
|
+
onKeyDown={(e) => {
|
|
70
|
+
if (e.key === 'Enter' && (e.metaKey || e.ctrlKey)) {
|
|
71
|
+
e.preventDefault();
|
|
72
|
+
onSubmit();
|
|
73
|
+
}
|
|
74
|
+
}}
|
|
75
|
+
/>
|
|
76
|
+
{error && <p className="mt-1 text-xs text-red-600">{error}</p>}
|
|
77
|
+
<div className="mt-2 flex items-center justify-end gap-2">
|
|
78
|
+
<button
|
|
79
|
+
type="button"
|
|
80
|
+
onClick={() => setPending(null)}
|
|
81
|
+
className="rounded border px-2 py-1 text-xs hover:bg-muted"
|
|
82
|
+
>
|
|
83
|
+
Cancel
|
|
84
|
+
</button>
|
|
85
|
+
<button
|
|
86
|
+
type="button"
|
|
87
|
+
disabled={submitting || !text.trim()}
|
|
88
|
+
onClick={onSubmit}
|
|
89
|
+
className="rounded bg-primary px-3 py-1 text-xs font-medium text-primary-foreground disabled:opacity-50"
|
|
90
|
+
>
|
|
91
|
+
{submitting ? 'Saving…' : 'Submit'}
|
|
92
|
+
</button>
|
|
93
|
+
</div>
|
|
94
|
+
<p className="mt-2 text-[10px] text-muted-foreground">⌘/Ctrl + Enter to submit</p>
|
|
95
|
+
</div>,
|
|
96
|
+
document.body,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function clamp(n: number, lo: number, hi: number): number {
|
|
101
|
+
return Math.max(lo, Math.min(hi, n));
|
|
102
|
+
}
|
|
@@ -0,0 +1,63 @@
|
|
|
1
|
+
import { MessageSquare, Trash2, X } from 'lucide-react';
|
|
2
|
+
import { useState } from 'react';
|
|
3
|
+
import { useInspector } from './InspectorProvider';
|
|
4
|
+
|
|
5
|
+
export function CommentWidget() {
|
|
6
|
+
const { comments, remove, error } = useInspector();
|
|
7
|
+
const [open, setOpen] = useState(false);
|
|
8
|
+
const count = comments.length;
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div data-inspector-ui className="fixed right-4 bottom-4 z-40">
|
|
12
|
+
{open && (
|
|
13
|
+
<div className="mb-2 w-80 rounded-md border bg-card shadow-xl">
|
|
14
|
+
<div className="flex items-center justify-between border-b px-3 py-2">
|
|
15
|
+
<span className="text-xs font-semibold">
|
|
16
|
+
{count} comment{count === 1 ? '' : 's'}
|
|
17
|
+
</span>
|
|
18
|
+
<button
|
|
19
|
+
type="button"
|
|
20
|
+
className="text-muted-foreground hover:text-foreground"
|
|
21
|
+
onClick={() => setOpen(false)}
|
|
22
|
+
>
|
|
23
|
+
<X className="size-3.5" />
|
|
24
|
+
</button>
|
|
25
|
+
</div>
|
|
26
|
+
{error && <p className="px-3 py-2 text-xs text-red-600">{error}</p>}
|
|
27
|
+
{count === 0 ? (
|
|
28
|
+
<p className="px-3 py-6 text-center text-xs text-muted-foreground">
|
|
29
|
+
No comments yet. Toggle Inspect and click a slide element.
|
|
30
|
+
</p>
|
|
31
|
+
) : (
|
|
32
|
+
<ul className="max-h-72 overflow-auto">
|
|
33
|
+
{comments.map((c) => (
|
|
34
|
+
<li key={c.id} className="flex items-start gap-2 border-b px-3 py-2 last:border-0">
|
|
35
|
+
<div className="min-w-0 flex-1">
|
|
36
|
+
<div className="text-[10px] font-mono text-muted-foreground">line {c.line}</div>
|
|
37
|
+
<div className="mt-0.5 text-xs break-words">{c.note}</div>
|
|
38
|
+
</div>
|
|
39
|
+
<button
|
|
40
|
+
type="button"
|
|
41
|
+
onClick={() => remove(c.id)}
|
|
42
|
+
className="shrink-0 rounded p-1 text-muted-foreground hover:bg-muted hover:text-red-600"
|
|
43
|
+
title="Delete"
|
|
44
|
+
>
|
|
45
|
+
<Trash2 className="size-3.5" />
|
|
46
|
+
</button>
|
|
47
|
+
</li>
|
|
48
|
+
))}
|
|
49
|
+
</ul>
|
|
50
|
+
)}
|
|
51
|
+
</div>
|
|
52
|
+
)}
|
|
53
|
+
<button
|
|
54
|
+
type="button"
|
|
55
|
+
onClick={() => setOpen((o) => !o)}
|
|
56
|
+
className="flex items-center gap-2 rounded-full border bg-card px-3 py-2 text-xs font-medium shadow-lg hover:bg-muted"
|
|
57
|
+
>
|
|
58
|
+
<MessageSquare className="size-4" />
|
|
59
|
+
{count}
|
|
60
|
+
</button>
|
|
61
|
+
</div>
|
|
62
|
+
);
|
|
63
|
+
}
|
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { findSlideSource, type SlideSourceHit } from '@/lib/inspector/fiber';
|
|
3
|
+
import { CommentPopover } from './CommentPopover';
|
|
4
|
+
import { useInspector } from './InspectorProvider';
|
|
5
|
+
|
|
6
|
+
type Highlight = { rect: DOMRect; hit: SlideSourceHit };
|
|
7
|
+
|
|
8
|
+
export function InspectOverlay() {
|
|
9
|
+
const { active, deckId, pending, setPending } = useInspector();
|
|
10
|
+
const overlayRef = useRef<HTMLDivElement>(null);
|
|
11
|
+
const [hover, setHover] = useState<Highlight | null>(null);
|
|
12
|
+
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
if (!active) {
|
|
15
|
+
setHover(null);
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const onMove = (e: PointerEvent) => {
|
|
20
|
+
if (pending) return;
|
|
21
|
+
const el = pickElement(e.clientX, e.clientY);
|
|
22
|
+
if (!el) return setHover(null);
|
|
23
|
+
const hit = findSlideSource(el, deckId);
|
|
24
|
+
if (!hit) return setHover(null);
|
|
25
|
+
setHover({ rect: hit.anchor.getBoundingClientRect(), hit });
|
|
26
|
+
};
|
|
27
|
+
|
|
28
|
+
const onClick = (e: MouseEvent) => {
|
|
29
|
+
if (pending) return;
|
|
30
|
+
if (e.target instanceof Element && e.target.closest('[data-inspector-ui]')) return;
|
|
31
|
+
const el = pickElement(e.clientX, e.clientY);
|
|
32
|
+
if (!el) return;
|
|
33
|
+
const hit = findSlideSource(el, deckId);
|
|
34
|
+
if (!hit) return;
|
|
35
|
+
e.preventDefault();
|
|
36
|
+
e.stopPropagation();
|
|
37
|
+
setPending({
|
|
38
|
+
line: hit.line,
|
|
39
|
+
column: hit.column,
|
|
40
|
+
anchorRect: hit.anchor.getBoundingClientRect(),
|
|
41
|
+
clickX: e.clientX,
|
|
42
|
+
clickY: e.clientY,
|
|
43
|
+
});
|
|
44
|
+
setHover(null);
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
window.addEventListener('pointermove', onMove, true);
|
|
48
|
+
window.addEventListener('click', onClick, true);
|
|
49
|
+
return () => {
|
|
50
|
+
window.removeEventListener('pointermove', onMove, true);
|
|
51
|
+
window.removeEventListener('click', onClick, true);
|
|
52
|
+
};
|
|
53
|
+
}, [active, deckId, pending, setPending]);
|
|
54
|
+
|
|
55
|
+
if (!active) return null;
|
|
56
|
+
|
|
57
|
+
const overlayRect = overlayRef.current?.getBoundingClientRect();
|
|
58
|
+
const show = hover && overlayRect;
|
|
59
|
+
|
|
60
|
+
return (
|
|
61
|
+
<>
|
|
62
|
+
<div
|
|
63
|
+
ref={overlayRef}
|
|
64
|
+
className="pointer-events-none absolute inset-0 z-30"
|
|
65
|
+
style={{ cursor: 'crosshair' }}
|
|
66
|
+
>
|
|
67
|
+
{show && (
|
|
68
|
+
<div
|
|
69
|
+
className="absolute"
|
|
70
|
+
style={{
|
|
71
|
+
left: hover.rect.left - overlayRect.left,
|
|
72
|
+
top: hover.rect.top - overlayRect.top,
|
|
73
|
+
width: hover.rect.width,
|
|
74
|
+
height: hover.rect.height,
|
|
75
|
+
outline: '2px solid #3b82f6',
|
|
76
|
+
background: 'rgba(59,130,246,0.1)',
|
|
77
|
+
}}
|
|
78
|
+
/>
|
|
79
|
+
)}
|
|
80
|
+
</div>
|
|
81
|
+
{pending && <CommentPopover />}
|
|
82
|
+
</>
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
function pickElement(x: number, y: number): HTMLElement | null {
|
|
87
|
+
const stack = document.elementsFromPoint(x, y);
|
|
88
|
+
for (const el of stack) {
|
|
89
|
+
if (!(el instanceof HTMLElement)) continue;
|
|
90
|
+
if (el.closest('[data-inspector-ui]')) continue;
|
|
91
|
+
return el;
|
|
92
|
+
}
|
|
93
|
+
return null;
|
|
94
|
+
}
|
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import { Crosshair } from 'lucide-react';
|
|
2
|
+
import { createContext, type ReactNode, useCallback, useContext, useMemo, useState } from 'react';
|
|
3
|
+
import { Button } from '@/components/ui/button';
|
|
4
|
+
import { type SlideComment, useComments } from '@/lib/inspector/useComments';
|
|
5
|
+
|
|
6
|
+
export type PendingTarget = {
|
|
7
|
+
line: number;
|
|
8
|
+
column: number;
|
|
9
|
+
anchorRect: DOMRect;
|
|
10
|
+
clickX: number;
|
|
11
|
+
clickY: number;
|
|
12
|
+
};
|
|
13
|
+
|
|
14
|
+
type InspectorCtx = {
|
|
15
|
+
deckId: string;
|
|
16
|
+
active: boolean;
|
|
17
|
+
toggle: () => void;
|
|
18
|
+
comments: SlideComment[];
|
|
19
|
+
error: string | null;
|
|
20
|
+
refetch: () => Promise<void>;
|
|
21
|
+
add: (line: number, column: number, text: string) => Promise<void>;
|
|
22
|
+
remove: (id: string) => Promise<void>;
|
|
23
|
+
pending: PendingTarget | null;
|
|
24
|
+
setPending: (p: PendingTarget | null) => void;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
const Ctx = createContext<InspectorCtx | null>(null);
|
|
28
|
+
|
|
29
|
+
export function useInspector(): InspectorCtx {
|
|
30
|
+
const v = useContext(Ctx);
|
|
31
|
+
if (!v) throw new Error('useInspector must be used inside <InspectorProvider>');
|
|
32
|
+
return v;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
export function InspectorProvider({ deckId, children }: { deckId: string; children: ReactNode }) {
|
|
36
|
+
const [active, setActive] = useState(false);
|
|
37
|
+
const [pending, setPending] = useState<PendingTarget | null>(null);
|
|
38
|
+
const { comments, error, refetch, add, remove } = useComments(deckId);
|
|
39
|
+
|
|
40
|
+
const toggle = useCallback(() => {
|
|
41
|
+
setActive((a) => {
|
|
42
|
+
if (a) setPending(null);
|
|
43
|
+
return !a;
|
|
44
|
+
});
|
|
45
|
+
}, []);
|
|
46
|
+
|
|
47
|
+
const value = useMemo<InspectorCtx>(
|
|
48
|
+
() => ({
|
|
49
|
+
deckId,
|
|
50
|
+
active,
|
|
51
|
+
toggle,
|
|
52
|
+
comments,
|
|
53
|
+
error,
|
|
54
|
+
refetch,
|
|
55
|
+
add,
|
|
56
|
+
remove,
|
|
57
|
+
pending,
|
|
58
|
+
setPending,
|
|
59
|
+
}),
|
|
60
|
+
[deckId, active, toggle, comments, error, refetch, add, remove, pending],
|
|
61
|
+
);
|
|
62
|
+
|
|
63
|
+
return <Ctx.Provider value={value}>{children}</Ctx.Provider>;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
export function InspectToggleButton() {
|
|
67
|
+
const { active, toggle } = useInspector();
|
|
68
|
+
if (import.meta.env.PROD) return null;
|
|
69
|
+
return (
|
|
70
|
+
<Button size="sm" variant={active ? 'default' : 'outline'} onClick={toggle} data-inspector-ui>
|
|
71
|
+
<Crosshair className="size-4" />
|
|
72
|
+
Inspect
|
|
73
|
+
</Button>
|
|
74
|
+
);
|
|
75
|
+
}
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import { Slot } from 'radix-ui';
|
|
4
|
+
|
|
5
|
+
import { cn } from '@/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-4xl border border-transparent px-2 py-0.5 text-xs 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-3!',
|
|
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: 'border-border text-foreground [a]:hover:bg-muted [a]:hover:text-muted-foreground',
|
|
17
|
+
ghost: 'hover:bg-muted hover:text-muted-foreground dark:hover:bg-muted/50',
|
|
18
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
19
|
+
},
|
|
20
|
+
},
|
|
21
|
+
defaultVariants: {
|
|
22
|
+
variant: 'default',
|
|
23
|
+
},
|
|
24
|
+
},
|
|
25
|
+
);
|
|
26
|
+
|
|
27
|
+
function Badge({
|
|
28
|
+
className,
|
|
29
|
+
variant = 'default',
|
|
30
|
+
asChild = false,
|
|
31
|
+
...props
|
|
32
|
+
}: React.ComponentProps<'span'> & VariantProps<typeof badgeVariants> & { asChild?: boolean }) {
|
|
33
|
+
const Comp = asChild ? Slot.Root : 'span';
|
|
34
|
+
|
|
35
|
+
return (
|
|
36
|
+
<Comp
|
|
37
|
+
data-slot="badge"
|
|
38
|
+
data-variant={variant}
|
|
39
|
+
className={cn(badgeVariants({ variant }), className)}
|
|
40
|
+
{...props}
|
|
41
|
+
/>
|
|
42
|
+
);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
export { Badge, badgeVariants };
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { cva, type VariantProps } from 'class-variance-authority';
|
|
3
|
+
import { Slot } from 'radix-ui';
|
|
4
|
+
|
|
5
|
+
import { cn } from '@/lib/utils';
|
|
6
|
+
|
|
7
|
+
const buttonVariants = cva(
|
|
8
|
+
"group/button inline-flex shrink-0 items-center justify-center rounded-lg border border-transparent bg-clip-padding text-sm font-medium whitespace-nowrap transition-all outline-none select-none focus-visible:border-ring focus-visible:ring-3 focus-visible:ring-ring/50 active:not-aria-[haspopup]:translate-y-px disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-3 aria-invalid:ring-destructive/20 dark:aria-invalid:border-destructive/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
|
|
9
|
+
{
|
|
10
|
+
variants: {
|
|
11
|
+
variant: {
|
|
12
|
+
default: 'bg-primary text-primary-foreground [a]:hover:bg-primary/80',
|
|
13
|
+
outline:
|
|
14
|
+
'border-border bg-background hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:border-input dark:bg-input/30 dark:hover:bg-input/50',
|
|
15
|
+
secondary:
|
|
16
|
+
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary aria-expanded:text-secondary-foreground',
|
|
17
|
+
ghost:
|
|
18
|
+
'hover:bg-muted hover:text-foreground aria-expanded:bg-muted aria-expanded:text-foreground dark:hover:bg-muted/50',
|
|
19
|
+
destructive:
|
|
20
|
+
'bg-destructive/10 text-destructive hover:bg-destructive/20 focus-visible:border-destructive/40 focus-visible:ring-destructive/20 dark:bg-destructive/20 dark:hover:bg-destructive/30 dark:focus-visible:ring-destructive/40',
|
|
21
|
+
link: 'text-primary underline-offset-4 hover:underline',
|
|
22
|
+
},
|
|
23
|
+
size: {
|
|
24
|
+
default:
|
|
25
|
+
'h-8 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
|
26
|
+
xs: "h-6 gap-1 rounded-[min(var(--radius-md),10px)] px-2 text-xs in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3",
|
|
27
|
+
sm: "h-7 gap-1 rounded-[min(var(--radius-md),12px)] px-2.5 text-[0.8rem] in-data-[slot=button-group]:rounded-lg has-data-[icon=inline-end]:pr-1.5 has-data-[icon=inline-start]:pl-1.5 [&_svg:not([class*='size-'])]:size-3.5",
|
|
28
|
+
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
|
29
|
+
icon: 'size-8',
|
|
30
|
+
'icon-xs':
|
|
31
|
+
"size-6 rounded-[min(var(--radius-md),10px)] in-data-[slot=button-group]:rounded-lg [&_svg:not([class*='size-'])]:size-3",
|
|
32
|
+
'icon-sm':
|
|
33
|
+
'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
|
|
34
|
+
'icon-lg': 'size-9',
|
|
35
|
+
},
|
|
36
|
+
},
|
|
37
|
+
defaultVariants: {
|
|
38
|
+
variant: 'default',
|
|
39
|
+
size: 'default',
|
|
40
|
+
},
|
|
41
|
+
},
|
|
42
|
+
);
|
|
43
|
+
|
|
44
|
+
function Button({
|
|
45
|
+
className,
|
|
46
|
+
variant = 'default',
|
|
47
|
+
size = 'default',
|
|
48
|
+
asChild = false,
|
|
49
|
+
...props
|
|
50
|
+
}: React.ComponentProps<'button'> &
|
|
51
|
+
VariantProps<typeof buttonVariants> & {
|
|
52
|
+
asChild?: boolean;
|
|
53
|
+
}) {
|
|
54
|
+
const Comp = asChild ? Slot.Root : 'button';
|
|
55
|
+
|
|
56
|
+
return (
|
|
57
|
+
<Comp
|
|
58
|
+
data-slot="button"
|
|
59
|
+
data-variant={variant}
|
|
60
|
+
data-size={size}
|
|
61
|
+
className={cn(buttonVariants({ variant, size, className }))}
|
|
62
|
+
{...props}
|
|
63
|
+
/>
|
|
64
|
+
);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export { Button, buttonVariants };
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
|
|
5
|
+
function Card({
|
|
6
|
+
className,
|
|
7
|
+
size = 'default',
|
|
8
|
+
...props
|
|
9
|
+
}: React.ComponentProps<'div'> & { size?: 'default' | 'sm' }) {
|
|
10
|
+
return (
|
|
11
|
+
<div
|
|
12
|
+
data-slot="card"
|
|
13
|
+
data-size={size}
|
|
14
|
+
className={cn(
|
|
15
|
+
'group/card flex flex-col gap-4 overflow-hidden rounded-xl bg-card py-4 text-sm text-card-foreground ring-1 ring-foreground/10 has-data-[slot=card-footer]:pb-0 has-[>img:first-child]:pt-0 data-[size=sm]:gap-3 data-[size=sm]:py-3 data-[size=sm]:has-data-[slot=card-footer]:pb-0 *:[img:first-child]:rounded-t-xl *:[img:last-child]:rounded-b-xl',
|
|
16
|
+
className,
|
|
17
|
+
)}
|
|
18
|
+
{...props}
|
|
19
|
+
/>
|
|
20
|
+
);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function CardHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|
24
|
+
return (
|
|
25
|
+
<div
|
|
26
|
+
data-slot="card-header"
|
|
27
|
+
className={cn(
|
|
28
|
+
'group/card-header @container/card-header grid auto-rows-min items-start gap-1 rounded-t-xl px-4 group-data-[size=sm]/card:px-3 has-data-[slot=card-action]:grid-cols-[1fr_auto] has-data-[slot=card-description]:grid-rows-[auto_auto] [.border-b]:pb-4 group-data-[size=sm]/card:[.border-b]:pb-3',
|
|
29
|
+
className,
|
|
30
|
+
)}
|
|
31
|
+
{...props}
|
|
32
|
+
/>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function CardTitle({ className, ...props }: React.ComponentProps<'div'>) {
|
|
37
|
+
return (
|
|
38
|
+
<div
|
|
39
|
+
data-slot="card-title"
|
|
40
|
+
className={cn(
|
|
41
|
+
'font-heading text-base leading-snug font-medium group-data-[size=sm]/card:text-sm',
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
{...props}
|
|
45
|
+
/>
|
|
46
|
+
);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function CardDescription({ className, ...props }: React.ComponentProps<'div'>) {
|
|
50
|
+
return (
|
|
51
|
+
<div
|
|
52
|
+
data-slot="card-description"
|
|
53
|
+
className={cn('text-sm text-muted-foreground', className)}
|
|
54
|
+
{...props}
|
|
55
|
+
/>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function CardAction({ className, ...props }: React.ComponentProps<'div'>) {
|
|
60
|
+
return (
|
|
61
|
+
<div
|
|
62
|
+
data-slot="card-action"
|
|
63
|
+
className={cn('col-start-2 row-span-2 row-start-1 self-start justify-self-end', className)}
|
|
64
|
+
{...props}
|
|
65
|
+
/>
|
|
66
|
+
);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function CardContent({ className, ...props }: React.ComponentProps<'div'>) {
|
|
70
|
+
return (
|
|
71
|
+
<div
|
|
72
|
+
data-slot="card-content"
|
|
73
|
+
className={cn('px-4 group-data-[size=sm]/card:px-3', className)}
|
|
74
|
+
{...props}
|
|
75
|
+
/>
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function CardFooter({ className, ...props }: React.ComponentProps<'div'>) {
|
|
80
|
+
return (
|
|
81
|
+
<div
|
|
82
|
+
data-slot="card-footer"
|
|
83
|
+
className={cn(
|
|
84
|
+
'flex items-center rounded-b-xl border-t bg-muted/50 p-4 group-data-[size=sm]/card:p-3',
|
|
85
|
+
className,
|
|
86
|
+
)}
|
|
87
|
+
{...props}
|
|
88
|
+
/>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
export { Card, CardHeader, CardFooter, CardTitle, CardAction, CardDescription, CardContent };
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import * as React from 'react';
|
|
2
|
+
import { ScrollArea as ScrollAreaPrimitive } from 'radix-ui';
|
|
3
|
+
|
|
4
|
+
import { cn } from '@/lib/utils';
|
|
5
|
+
|
|
6
|
+
function ScrollArea({
|
|
7
|
+
className,
|
|
8
|
+
children,
|
|
9
|
+
...props
|
|
10
|
+
}: React.ComponentProps<typeof ScrollAreaPrimitive.Root>) {
|
|
11
|
+
return (
|
|
12
|
+
<ScrollAreaPrimitive.Root
|
|
13
|
+
data-slot="scroll-area"
|
|
14
|
+
className={cn('relative', className)}
|
|
15
|
+
{...props}
|
|
16
|
+
>
|
|
17
|
+
<ScrollAreaPrimitive.Viewport
|
|
18
|
+
data-slot="scroll-area-viewport"
|
|
19
|
+
className="size-full rounded-[inherit] transition-[color,box-shadow] outline-none focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1"
|
|
20
|
+
>
|
|
21
|
+
{children}
|
|
22
|
+
</ScrollAreaPrimitive.Viewport>
|
|
23
|
+
<ScrollBar />
|
|
24
|
+
<ScrollAreaPrimitive.Corner />
|
|
25
|
+
</ScrollAreaPrimitive.Root>
|
|
26
|
+
);
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function ScrollBar({
|
|
30
|
+
className,
|
|
31
|
+
orientation = 'vertical',
|
|
32
|
+
...props
|
|
33
|
+
}: React.ComponentProps<typeof ScrollAreaPrimitive.ScrollAreaScrollbar>) {
|
|
34
|
+
return (
|
|
35
|
+
<ScrollAreaPrimitive.ScrollAreaScrollbar
|
|
36
|
+
data-slot="scroll-area-scrollbar"
|
|
37
|
+
data-orientation={orientation}
|
|
38
|
+
orientation={orientation}
|
|
39
|
+
className={cn(
|
|
40
|
+
'flex touch-none p-px transition-colors select-none data-horizontal:h-2.5 data-horizontal:flex-col data-horizontal:border-t data-horizontal:border-t-transparent data-vertical:h-full data-vertical:w-2.5 data-vertical:border-l data-vertical:border-l-transparent',
|
|
41
|
+
className,
|
|
42
|
+
)}
|
|
43
|
+
{...props}
|
|
44
|
+
>
|
|
45
|
+
<ScrollAreaPrimitive.ScrollAreaThumb
|
|
46
|
+
data-slot="scroll-area-thumb"
|
|
47
|
+
className="relative flex-1 rounded-full bg-border"
|
|
48
|
+
/>
|
|
49
|
+
</ScrollAreaPrimitive.ScrollAreaScrollbar>
|
|
50
|
+
);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
export { ScrollArea, ScrollBar };
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import * as React from 'react';
|
|
4
|
+
import { Separator as SeparatorPrimitive } from 'radix-ui';
|
|
5
|
+
|
|
6
|
+
import { cn } from '@/lib/utils';
|
|
7
|
+
|
|
8
|
+
function Separator({
|
|
9
|
+
className,
|
|
10
|
+
orientation = 'horizontal',
|
|
11
|
+
decorative = true,
|
|
12
|
+
...props
|
|
13
|
+
}: React.ComponentProps<typeof SeparatorPrimitive.Root>) {
|
|
14
|
+
return (
|
|
15
|
+
<SeparatorPrimitive.Root
|
|
16
|
+
data-slot="separator"
|
|
17
|
+
decorative={decorative}
|
|
18
|
+
orientation={orientation}
|
|
19
|
+
className={cn(
|
|
20
|
+
'shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch',
|
|
21
|
+
className,
|
|
22
|
+
)}
|
|
23
|
+
{...props}
|
|
24
|
+
/>
|
|
25
|
+
);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
export { Separator };
|