@open-slide/core 0.0.10 → 0.0.12
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-DHiRlpjn.js → build-aiY_8kwE.js} +2 -1
- package/dist/cli/bin.js +43 -4
- package/dist/{config-LZM903FE.js → config-CVqRAagl.js} +592 -63
- package/dist/design-CROQh0AA.js +35 -0
- package/dist/{dev-B3JzCYn7.js → dev-R2we2iaF.js} +2 -1
- package/dist/index.d.ts +55 -4
- package/dist/index.js +110 -1
- package/dist/{preview-UikovHEt.js → preview-CU4zSyGp.js} +2 -1
- package/dist/sync-3oqN1WyK.js +139 -0
- package/dist/sync-B4eLo2H6.js +3 -0
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +2 -1
- package/package.json +2 -1
- package/skills/apply-comments/SKILL.md +83 -0
- package/skills/create-slide/SKILL.md +81 -0
- package/skills/create-theme/SKILL.md +194 -0
- package/skills/slide-authoring/SKILL.md +288 -0
- package/src/app/{App.tsx → app.tsx} +8 -6
- package/src/app/components/{AssetView.tsx → asset-view.tsx} +41 -33
- package/src/app/components/{ClickNavZones.tsx → click-nav-zones.tsx} +1 -1
- package/src/app/components/history-provider.tsx +120 -0
- package/src/app/components/image-placeholder.tsx +121 -0
- package/src/app/components/inspector/{CommentWidget.tsx → comment-widget.tsx} +1 -1
- package/src/app/components/inspector/{InspectOverlay.tsx → inspect-overlay.tsx} +1 -1
- package/src/app/components/inspector/{InspectorPanel.tsx → inspector-panel.tsx} +164 -212
- package/src/app/components/inspector/{InspectorProvider.tsx → inspector-provider.tsx} +186 -18
- package/src/app/components/inspector/save-bar.tsx +47 -0
- package/src/app/components/panel/panel-fields.tsx +60 -0
- package/src/app/components/panel/panel-shell.tsx +78 -0
- package/src/app/components/panel/save-card.tsx +139 -0
- package/src/app/components/pdf-progress-toast.tsx +25 -0
- package/src/app/components/player.tsx +341 -0
- package/src/app/components/present/blackout-overlay.tsx +18 -0
- package/src/app/components/present/control-bar.tsx +204 -0
- package/src/app/components/present/help-overlay.tsx +56 -0
- package/src/app/components/present/jump-input.tsx +74 -0
- package/src/app/components/present/laser-pointer.tsx +40 -0
- package/src/app/components/present/overview-grid.tsx +184 -0
- package/src/app/components/present/progress-bar.tsx +26 -0
- package/src/app/components/present/use-idle.ts +44 -0
- package/src/app/components/present/use-pointer-near-bottom.ts +34 -0
- package/src/app/components/present/use-presenter-channel.ts +71 -0
- package/src/app/components/present/use-touch-swipe.ts +63 -0
- package/src/app/components/sidebar/{FolderItem.tsx → folder-item.tsx} +62 -27
- package/src/app/components/sidebar/{IconPicker.tsx → icon-picker.tsx} +13 -10
- package/src/app/components/sidebar/{Sidebar.tsx → sidebar.tsx} +40 -34
- package/src/app/components/{SlideCanvas.tsx → slide-canvas.tsx} +35 -10
- package/src/app/components/style-panel/design-provider.tsx +139 -0
- package/src/app/components/style-panel/style-panel.tsx +326 -0
- package/src/app/components/style-panel/use-design.ts +112 -0
- package/src/app/components/theme-toggle.tsx +57 -0
- package/src/app/components/thumbnail-rail.tsx +151 -0
- package/src/app/components/ui/button.tsx +51 -19
- package/src/app/components/ui/card.tsx +1 -1
- package/src/app/components/ui/dialog.tsx +25 -9
- package/src/app/components/ui/dropdown-menu.tsx +29 -12
- package/src/app/components/ui/input.tsx +13 -9
- package/src/app/components/ui/popover.tsx +5 -2
- package/src/app/components/ui/progress.tsx +2 -2
- package/src/app/components/ui/select.tsx +11 -5
- package/src/app/components/ui/separator.tsx +1 -1
- package/src/app/components/ui/slider.tsx +4 -4
- package/src/app/components/ui/sonner.tsx +11 -1
- package/src/app/components/ui/tabs.tsx +6 -6
- package/src/app/components/ui/textarea.tsx +11 -7
- package/src/app/components/ui/toggle-group.tsx +2 -2
- package/src/app/components/ui/toggle.tsx +6 -6
- package/src/app/components/ui/tooltip.tsx +5 -2
- package/src/app/lib/export-html.ts +10 -1
- package/src/app/lib/export-pdf.ts +7 -0
- package/src/app/lib/folders.ts +1 -1
- package/src/app/lib/inspector/{useEditor.ts → use-editor.ts} +2 -1
- package/src/app/lib/sdk.ts +5 -0
- package/src/app/lib/slides.ts +1 -1
- package/src/app/lib/utils.ts +1 -1
- package/src/app/main.tsx +5 -2
- package/src/app/routes/{Home.tsx → home.tsx} +266 -97
- package/src/app/routes/presenter.tsx +400 -0
- package/src/app/routes/slide.tsx +519 -0
- package/src/app/styles.css +338 -67
- package/src/app/components/PdfProgressToast.tsx +0 -23
- package/src/app/components/Player.tsx +0 -100
- package/src/app/components/ThumbnailRail.tsx +0 -68
- package/src/app/components/inspector/SaveBar.tsx +0 -77
- package/src/app/routes/Slide.tsx +0 -478
- /package/dist/{config-SXL5qIl6.d.ts → config-DweCbRkQ.d.ts} +0 -0
- /package/src/app/lib/inspector/{useComments.ts → use-comments.ts} +0 -0
- /package/src/app/lib/{useWheelPageNavigation.ts → use-wheel-page-navigation.ts} +0 -0
|
@@ -0,0 +1,112 @@
|
|
|
1
|
+
import { useCallback, useEffect, useRef, useState } from 'react';
|
|
2
|
+
import type { DesignSystem } from '../../../design';
|
|
3
|
+
|
|
4
|
+
type FetchedState = {
|
|
5
|
+
design: DesignSystem | null;
|
|
6
|
+
exists: boolean;
|
|
7
|
+
warning: string | null;
|
|
8
|
+
loaded: boolean;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
export type UseDesignReturn = FetchedState & {
|
|
12
|
+
refresh: () => Promise<void>;
|
|
13
|
+
save: (patch: Partial<DesignSystem>) => Promise<{ ok: boolean; error?: string }>;
|
|
14
|
+
reset: () => Promise<{ ok: boolean; error?: string }>;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
export function useDesign(slideId: string): UseDesignReturn {
|
|
18
|
+
const [state, setState] = useState<FetchedState>({
|
|
19
|
+
design: null,
|
|
20
|
+
exists: false,
|
|
21
|
+
warning: null,
|
|
22
|
+
loaded: false,
|
|
23
|
+
});
|
|
24
|
+
const slideIdRef = useRef(slideId);
|
|
25
|
+
slideIdRef.current = slideId;
|
|
26
|
+
|
|
27
|
+
const refresh = useCallback(async () => {
|
|
28
|
+
const id = slideIdRef.current;
|
|
29
|
+
if (!id) return;
|
|
30
|
+
try {
|
|
31
|
+
const res = await fetch(`/__design?slideId=${encodeURIComponent(id)}`);
|
|
32
|
+
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
33
|
+
const body = (await res.json()) as {
|
|
34
|
+
design: DesignSystem;
|
|
35
|
+
exists: boolean;
|
|
36
|
+
warning: string | null;
|
|
37
|
+
};
|
|
38
|
+
setState({
|
|
39
|
+
design: body.design,
|
|
40
|
+
exists: body.exists,
|
|
41
|
+
warning: body.warning,
|
|
42
|
+
loaded: true,
|
|
43
|
+
});
|
|
44
|
+
} catch (err) {
|
|
45
|
+
setState((s) => ({ ...s, warning: String((err as Error).message), loaded: true }));
|
|
46
|
+
}
|
|
47
|
+
}, []);
|
|
48
|
+
|
|
49
|
+
useEffect(() => {
|
|
50
|
+
setState({ design: null, exists: false, warning: null, loaded: false });
|
|
51
|
+
void refresh();
|
|
52
|
+
}, [refresh]);
|
|
53
|
+
|
|
54
|
+
const save = useCallback(async (patch: Partial<DesignSystem>) => {
|
|
55
|
+
const id = slideIdRef.current;
|
|
56
|
+
if (!id) return { ok: false, error: 'no slide id' };
|
|
57
|
+
try {
|
|
58
|
+
const res = await fetch(`/__design?slideId=${encodeURIComponent(id)}`, {
|
|
59
|
+
method: 'PUT',
|
|
60
|
+
headers: { 'content-type': 'application/json' },
|
|
61
|
+
body: JSON.stringify({ patch }),
|
|
62
|
+
});
|
|
63
|
+
const body = (await res.json()) as {
|
|
64
|
+
ok?: boolean;
|
|
65
|
+
error?: string;
|
|
66
|
+
design?: DesignSystem;
|
|
67
|
+
created?: boolean;
|
|
68
|
+
};
|
|
69
|
+
if (!res.ok || !body.ok) {
|
|
70
|
+
return { ok: false, error: body.error ?? `HTTP ${res.status}` };
|
|
71
|
+
}
|
|
72
|
+
if (body.design) {
|
|
73
|
+
setState((s) => ({
|
|
74
|
+
...s,
|
|
75
|
+
design: body.design ?? s.design,
|
|
76
|
+
exists: true,
|
|
77
|
+
warning: null,
|
|
78
|
+
}));
|
|
79
|
+
}
|
|
80
|
+
return { ok: true };
|
|
81
|
+
} catch (err) {
|
|
82
|
+
return { ok: false, error: String((err as Error).message) };
|
|
83
|
+
}
|
|
84
|
+
}, []);
|
|
85
|
+
|
|
86
|
+
const reset = useCallback(async () => {
|
|
87
|
+
const id = slideIdRef.current;
|
|
88
|
+
if (!id) return { ok: false, error: 'no slide id' };
|
|
89
|
+
try {
|
|
90
|
+
const res = await fetch(`/__design/reset?slideId=${encodeURIComponent(id)}`, {
|
|
91
|
+
method: 'POST',
|
|
92
|
+
});
|
|
93
|
+
const body = (await res.json()) as { ok?: boolean; error?: string; design?: DesignSystem };
|
|
94
|
+
if (!res.ok || !body.ok) {
|
|
95
|
+
return { ok: false, error: body.error ?? `HTTP ${res.status}` };
|
|
96
|
+
}
|
|
97
|
+
if (body.design) {
|
|
98
|
+
setState((s) => ({
|
|
99
|
+
...s,
|
|
100
|
+
design: body.design ?? s.design,
|
|
101
|
+
exists: true,
|
|
102
|
+
warning: null,
|
|
103
|
+
}));
|
|
104
|
+
}
|
|
105
|
+
return { ok: true };
|
|
106
|
+
} catch (err) {
|
|
107
|
+
return { ok: false, error: String((err as Error).message) };
|
|
108
|
+
}
|
|
109
|
+
}, []);
|
|
110
|
+
|
|
111
|
+
return { ...state, refresh, save, reset };
|
|
112
|
+
}
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
import { Monitor, Moon, Sun } from 'lucide-react';
|
|
2
|
+
import { useTheme } from 'next-themes';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { buttonVariants } from '@/components/ui/button';
|
|
5
|
+
import {
|
|
6
|
+
DropdownMenu,
|
|
7
|
+
DropdownMenuContent,
|
|
8
|
+
DropdownMenuItem,
|
|
9
|
+
DropdownMenuTrigger,
|
|
10
|
+
} from '@/components/ui/dropdown-menu';
|
|
11
|
+
import { cn } from '@/lib/utils';
|
|
12
|
+
|
|
13
|
+
export function ThemeToggle() {
|
|
14
|
+
const { theme, setTheme } = useTheme();
|
|
15
|
+
const [mounted, setMounted] = useState(false);
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
setMounted(true);
|
|
19
|
+
}, []);
|
|
20
|
+
|
|
21
|
+
return (
|
|
22
|
+
<DropdownMenu>
|
|
23
|
+
<DropdownMenuTrigger
|
|
24
|
+
type="button"
|
|
25
|
+
aria-label="Toggle theme"
|
|
26
|
+
title="Theme"
|
|
27
|
+
className={cn(buttonVariants({ variant: 'ghost', size: 'icon-sm' }), 'relative')}
|
|
28
|
+
>
|
|
29
|
+
<Sun className="size-3.5 scale-100 rotate-0 transition-all dark:scale-0 dark:-rotate-90" />
|
|
30
|
+
<Moon className="absolute size-3.5 scale-0 rotate-90 transition-all dark:scale-100 dark:rotate-0" />
|
|
31
|
+
</DropdownMenuTrigger>
|
|
32
|
+
<DropdownMenuContent align="end" className="min-w-[140px]">
|
|
33
|
+
<DropdownMenuItem
|
|
34
|
+
onSelect={() => setTheme('light')}
|
|
35
|
+
data-active={mounted && theme === 'light'}
|
|
36
|
+
>
|
|
37
|
+
<Sun />
|
|
38
|
+
Light
|
|
39
|
+
</DropdownMenuItem>
|
|
40
|
+
<DropdownMenuItem
|
|
41
|
+
onSelect={() => setTheme('dark')}
|
|
42
|
+
data-active={mounted && theme === 'dark'}
|
|
43
|
+
>
|
|
44
|
+
<Moon />
|
|
45
|
+
Dark
|
|
46
|
+
</DropdownMenuItem>
|
|
47
|
+
<DropdownMenuItem
|
|
48
|
+
onSelect={() => setTheme('system')}
|
|
49
|
+
data-active={mounted && theme === 'system'}
|
|
50
|
+
>
|
|
51
|
+
<Monitor />
|
|
52
|
+
System
|
|
53
|
+
</DropdownMenuItem>
|
|
54
|
+
</DropdownMenuContent>
|
|
55
|
+
</DropdownMenu>
|
|
56
|
+
);
|
|
57
|
+
}
|
|
@@ -0,0 +1,151 @@
|
|
|
1
|
+
import { useEffect, useRef } from 'react';
|
|
2
|
+
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
3
|
+
import { cn } from '@/lib/utils';
|
|
4
|
+
import type { DesignSystem } from '../../design';
|
|
5
|
+
import type { Page } from '../lib/sdk';
|
|
6
|
+
import { CANVAS_HEIGHT, CANVAS_WIDTH } from '../lib/sdk';
|
|
7
|
+
import { SlideCanvas } from './slide-canvas';
|
|
8
|
+
|
|
9
|
+
type Orientation = 'vertical' | 'horizontal';
|
|
10
|
+
|
|
11
|
+
type Props = {
|
|
12
|
+
pages: Page[];
|
|
13
|
+
design?: DesignSystem;
|
|
14
|
+
current: number;
|
|
15
|
+
onSelect: (index: number) => void;
|
|
16
|
+
orientation?: Orientation;
|
|
17
|
+
};
|
|
18
|
+
|
|
19
|
+
const VERTICAL_THUMB_WIDTH = 184;
|
|
20
|
+
const HORIZONTAL_THUMB_HEIGHT = 64;
|
|
21
|
+
|
|
22
|
+
export function ThumbnailRail({
|
|
23
|
+
pages,
|
|
24
|
+
design,
|
|
25
|
+
current,
|
|
26
|
+
onSelect,
|
|
27
|
+
orientation = 'vertical',
|
|
28
|
+
}: Props) {
|
|
29
|
+
const activeRef = useRef<HTMLButtonElement | null>(null);
|
|
30
|
+
|
|
31
|
+
// biome-ignore lint/correctness/useExhaustiveDependencies: `current` triggers re-scroll on selection change
|
|
32
|
+
useEffect(() => {
|
|
33
|
+
const reduceMotion = window.matchMedia('(prefers-reduced-motion: reduce)').matches;
|
|
34
|
+
|
|
35
|
+
activeRef.current?.scrollIntoView({
|
|
36
|
+
block: 'nearest',
|
|
37
|
+
inline: 'nearest',
|
|
38
|
+
behavior: reduceMotion ? 'auto' : 'smooth',
|
|
39
|
+
});
|
|
40
|
+
}, [current]);
|
|
41
|
+
|
|
42
|
+
if (orientation === 'horizontal') {
|
|
43
|
+
const scale = HORIZONTAL_THUMB_HEIGHT / CANVAS_HEIGHT;
|
|
44
|
+
const width = CANVAS_WIDTH * scale;
|
|
45
|
+
return (
|
|
46
|
+
<div className="bg-sidebar">
|
|
47
|
+
<div className="overflow-x-auto overflow-y-hidden">
|
|
48
|
+
<div className="flex items-center gap-2 px-3 py-2.5">
|
|
49
|
+
{pages.map((PageComp, i) => {
|
|
50
|
+
const active = i === current;
|
|
51
|
+
return (
|
|
52
|
+
<button
|
|
53
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: pages list is render-stable
|
|
54
|
+
key={i}
|
|
55
|
+
type="button"
|
|
56
|
+
ref={active ? activeRef : undefined}
|
|
57
|
+
onClick={() => onSelect(i)}
|
|
58
|
+
aria-label={`Go to page ${i + 1}`}
|
|
59
|
+
aria-current={active ? 'true' : undefined}
|
|
60
|
+
className={cn('group/thumb relative flex shrink-0 flex-col items-center gap-1.5')}
|
|
61
|
+
>
|
|
62
|
+
<span
|
|
63
|
+
className={cn(
|
|
64
|
+
'font-mono text-[9.5px] font-medium tracking-[0.06em] tabular-nums uppercase',
|
|
65
|
+
active ? 'text-brand' : 'text-muted-foreground/70',
|
|
66
|
+
)}
|
|
67
|
+
>
|
|
68
|
+
{(i + 1).toString().padStart(2, '0')}
|
|
69
|
+
</span>
|
|
70
|
+
<div
|
|
71
|
+
className={cn(
|
|
72
|
+
'relative shrink-0 overflow-hidden rounded-[4px] border bg-card motion-safe:transition-all',
|
|
73
|
+
active
|
|
74
|
+
? 'border-brand shadow-[0_0_0_1px_var(--brand)]'
|
|
75
|
+
: 'border-hairline group-hover/thumb:border-foreground/25',
|
|
76
|
+
)}
|
|
77
|
+
style={{ width, height: HORIZONTAL_THUMB_HEIGHT }}
|
|
78
|
+
>
|
|
79
|
+
<SlideCanvas scale={scale} center={false} flat freezeMotion design={design}>
|
|
80
|
+
<PageComp />
|
|
81
|
+
</SlideCanvas>
|
|
82
|
+
</div>
|
|
83
|
+
</button>
|
|
84
|
+
);
|
|
85
|
+
})}
|
|
86
|
+
</div>
|
|
87
|
+
</div>
|
|
88
|
+
</div>
|
|
89
|
+
);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const scale = VERTICAL_THUMB_WIDTH / CANVAS_WIDTH;
|
|
93
|
+
const height = CANVAS_HEIGHT * scale;
|
|
94
|
+
return (
|
|
95
|
+
<ScrollArea className="h-full border-r border-hairline bg-sidebar">
|
|
96
|
+
<aside className="flex flex-col gap-2 px-3 py-3">
|
|
97
|
+
<div className="flex items-baseline justify-between px-1 pb-1">
|
|
98
|
+
<span className="eyebrow">Pages</span>
|
|
99
|
+
<span className="folio">{pages.length.toString().padStart(2, '0')}</span>
|
|
100
|
+
</div>
|
|
101
|
+
{pages.map((PageComp, i) => {
|
|
102
|
+
const active = i === current;
|
|
103
|
+
return (
|
|
104
|
+
<button
|
|
105
|
+
// biome-ignore lint/suspicious/noArrayIndexKey: pages list is render-stable
|
|
106
|
+
key={i}
|
|
107
|
+
type="button"
|
|
108
|
+
ref={active ? activeRef : undefined}
|
|
109
|
+
onClick={() => onSelect(i)}
|
|
110
|
+
aria-label={`Go to page ${i + 1}`}
|
|
111
|
+
aria-current={active ? 'true' : undefined}
|
|
112
|
+
className={cn(
|
|
113
|
+
'group/thumb flex items-start gap-2.5 rounded-[6px] p-1.5 text-left motion-safe:transition-colors',
|
|
114
|
+
'hover:bg-muted/60',
|
|
115
|
+
active && 'bg-muted',
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
<span
|
|
119
|
+
className={cn(
|
|
120
|
+
'mt-1.5 w-7 shrink-0 text-right font-mono text-[10px] font-medium tracking-[0.06em] tabular-nums uppercase',
|
|
121
|
+
active ? 'text-brand' : 'text-muted-foreground/70',
|
|
122
|
+
)}
|
|
123
|
+
>
|
|
124
|
+
{(i + 1).toString().padStart(2, '0')}
|
|
125
|
+
</span>
|
|
126
|
+
<div
|
|
127
|
+
className={cn(
|
|
128
|
+
'relative shrink-0 overflow-hidden rounded-[4px] border bg-card motion-safe:transition-all',
|
|
129
|
+
active
|
|
130
|
+
? 'border-brand shadow-[0_0_0_1px_var(--brand)]'
|
|
131
|
+
: 'border-hairline group-hover/thumb:border-foreground/25',
|
|
132
|
+
)}
|
|
133
|
+
style={{ width: VERTICAL_THUMB_WIDTH, height }}
|
|
134
|
+
>
|
|
135
|
+
<SlideCanvas scale={scale} center={false} flat freezeMotion design={design}>
|
|
136
|
+
<PageComp />
|
|
137
|
+
</SlideCanvas>
|
|
138
|
+
{active && (
|
|
139
|
+
<span
|
|
140
|
+
aria-hidden
|
|
141
|
+
className="pointer-events-none absolute inset-y-0 left-0 w-[2px] bg-brand"
|
|
142
|
+
/>
|
|
143
|
+
)}
|
|
144
|
+
</div>
|
|
145
|
+
</button>
|
|
146
|
+
);
|
|
147
|
+
})}
|
|
148
|
+
</aside>
|
|
149
|
+
</ScrollArea>
|
|
150
|
+
);
|
|
151
|
+
}
|
|
@@ -4,33 +4,65 @@ import { Slot } from 'radix-ui';
|
|
|
4
4
|
|
|
5
5
|
import { cn } from '@/lib/utils';
|
|
6
6
|
|
|
7
|
+
/*
|
|
8
|
+
* Editorial button. Tight square-ish radius, hairline borders instead of
|
|
9
|
+
* shadcn's default ring/shadow stack. The default variant is the strongest
|
|
10
|
+
* affordance — solid ink with subtle inner highlight on hover so the press
|
|
11
|
+
* feels physical without glow.
|
|
12
|
+
*/
|
|
7
13
|
const buttonVariants = cva(
|
|
8
|
-
|
|
14
|
+
[
|
|
15
|
+
"group/button relative inline-flex shrink-0 items-center justify-center",
|
|
16
|
+
"rounded-[6px] text-[13px] font-medium whitespace-nowrap select-none",
|
|
17
|
+
"outline-none transition-[background-color,color,border-color,box-shadow,transform] duration-100",
|
|
18
|
+
"focus-visible:ring-2 focus-visible:ring-ring/40 focus-visible:ring-offset-1 focus-visible:ring-offset-background",
|
|
19
|
+
"active:not-aria-[haspopup]:translate-y-px",
|
|
20
|
+
"disabled:pointer-events-none disabled:opacity-45",
|
|
21
|
+
"aria-invalid:border-destructive aria-invalid:ring-destructive/30",
|
|
22
|
+
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
|
|
23
|
+
].join(' '),
|
|
9
24
|
{
|
|
10
25
|
variants: {
|
|
11
26
|
variant: {
|
|
12
|
-
default:
|
|
13
|
-
|
|
14
|
-
'
|
|
27
|
+
default: [
|
|
28
|
+
'bg-foreground text-background',
|
|
29
|
+
'shadow-[inset_0_1px_0_oklch(1_0_0/0.12),0_1px_0_oklch(0_0_0/0.12)]',
|
|
30
|
+
'hover:bg-foreground/90',
|
|
31
|
+
'aria-expanded:bg-foreground/85',
|
|
32
|
+
].join(' '),
|
|
33
|
+
brand: [
|
|
34
|
+
'bg-brand text-brand-foreground',
|
|
35
|
+
'shadow-[inset_0_1px_0_oklch(1_0_0/0.18),0_1px_0_oklch(0_0_0/0.16)]',
|
|
36
|
+
'hover:brightness-105 active:brightness-95',
|
|
37
|
+
].join(' '),
|
|
38
|
+
outline: [
|
|
39
|
+
'border border-border bg-card text-foreground',
|
|
40
|
+
'hover:bg-muted/60 hover:border-foreground/20',
|
|
41
|
+
'aria-expanded:bg-muted aria-expanded:border-foreground/25',
|
|
42
|
+
'data-[state=on]:bg-foreground data-[state=on]:text-background data-[state=on]:border-foreground',
|
|
43
|
+
].join(' '),
|
|
15
44
|
secondary:
|
|
16
|
-
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary
|
|
17
|
-
ghost:
|
|
18
|
-
'
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
45
|
+
'bg-secondary text-secondary-foreground hover:bg-secondary/80 aria-expanded:bg-secondary/90',
|
|
46
|
+
ghost: [
|
|
47
|
+
'text-foreground/75 hover:text-foreground hover:bg-muted',
|
|
48
|
+
'aria-expanded:bg-muted aria-expanded:text-foreground',
|
|
49
|
+
].join(' '),
|
|
50
|
+
destructive: [
|
|
51
|
+
'bg-destructive text-white',
|
|
52
|
+
'shadow-[inset_0_1px_0_oklch(1_0_0/0.16),0_1px_0_oklch(0_0_0/0.12)]',
|
|
53
|
+
'hover:brightness-105 active:brightness-95',
|
|
54
|
+
'focus-visible:ring-destructive/35',
|
|
55
|
+
].join(' '),
|
|
56
|
+
link: 'text-foreground underline decoration-foreground/30 decoration-1 underline-offset-[3px] hover:decoration-foreground/70 [&_svg]:hidden',
|
|
22
57
|
},
|
|
23
58
|
size: {
|
|
24
|
-
default:
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
lg: 'h-9 gap-1.5 px-2.5 has-data-[icon=inline-end]:pr-2 has-data-[icon=inline-start]:pl-2',
|
|
59
|
+
default: 'h-8 gap-1.5 px-3',
|
|
60
|
+
xs: 'h-6 gap-1 rounded-[5px] px-2 text-[11.5px]',
|
|
61
|
+
sm: 'h-7 gap-1.5 rounded-[5px] px-2.5 text-[12px]',
|
|
62
|
+
lg: 'h-9 gap-1.5 px-3.5 text-[13.5px]',
|
|
29
63
|
icon: 'size-8',
|
|
30
|
-
'icon-xs':
|
|
31
|
-
|
|
32
|
-
'icon-sm':
|
|
33
|
-
'size-7 rounded-[min(var(--radius-md),12px)] in-data-[slot=button-group]:rounded-lg',
|
|
64
|
+
'icon-xs': 'size-6 rounded-[5px]',
|
|
65
|
+
'icon-sm': 'size-7 rounded-[5px]',
|
|
34
66
|
'icon-lg': 'size-9',
|
|
35
67
|
},
|
|
36
68
|
},
|
|
@@ -12,7 +12,7 @@ function Card({
|
|
|
12
12
|
data-slot="card"
|
|
13
13
|
data-size={size}
|
|
14
14
|
className={cn(
|
|
15
|
-
'group/card flex flex-col gap-4 overflow-hidden rounded-
|
|
15
|
+
'group/card flex flex-col gap-4 overflow-hidden rounded-[10px] bg-card py-4 text-sm text-card-foreground border border-border 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-[10px] *:[img:last-child]:rounded-b-[10px]',
|
|
16
16
|
className,
|
|
17
17
|
)}
|
|
18
18
|
{...props}
|
|
@@ -28,7 +28,7 @@ function DialogOverlay({
|
|
|
28
28
|
<DialogPrimitive.Overlay
|
|
29
29
|
data-slot="dialog-overlay"
|
|
30
30
|
className={cn(
|
|
31
|
-
'fixed inset-0 z-50 bg-
|
|
31
|
+
'fixed inset-0 z-50 bg-foreground/35 backdrop-blur-[2px] data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
|
|
32
32
|
className,
|
|
33
33
|
)}
|
|
34
34
|
{...props}
|
|
@@ -40,17 +40,26 @@ function DialogContent({
|
|
|
40
40
|
className,
|
|
41
41
|
children,
|
|
42
42
|
showCloseButton = true,
|
|
43
|
+
container,
|
|
43
44
|
...props
|
|
44
45
|
}: React.ComponentProps<typeof DialogPrimitive.Content> & {
|
|
45
46
|
showCloseButton?: boolean;
|
|
47
|
+
container?: React.ComponentProps<typeof DialogPrimitive.Portal>['container'];
|
|
46
48
|
}) {
|
|
47
49
|
return (
|
|
48
|
-
<DialogPortal data-slot="dialog-portal">
|
|
50
|
+
<DialogPortal data-slot="dialog-portal" container={container}>
|
|
49
51
|
<DialogOverlay />
|
|
50
52
|
<DialogPrimitive.Content
|
|
51
53
|
data-slot="dialog-content"
|
|
52
54
|
className={cn(
|
|
53
|
-
|
|
55
|
+
// Crisp paper card with hairline edge + soft drop. No oversized
|
|
56
|
+
// shadcn-glow ring; sits cleanly on the dimmed canvas.
|
|
57
|
+
'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-5',
|
|
58
|
+
'rounded-[10px] border border-border bg-card p-6 text-card-foreground',
|
|
59
|
+
'shadow-overlay outline-none',
|
|
60
|
+
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
|
61
|
+
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
|
62
|
+
'duration-200 sm:max-w-md',
|
|
54
63
|
className,
|
|
55
64
|
)}
|
|
56
65
|
{...props}
|
|
@@ -59,9 +68,10 @@ function DialogContent({
|
|
|
59
68
|
{showCloseButton && (
|
|
60
69
|
<DialogPrimitive.Close
|
|
61
70
|
data-slot="dialog-close"
|
|
62
|
-
|
|
71
|
+
aria-label="Close"
|
|
72
|
+
className="absolute top-3.5 right-3.5 inline-flex size-7 items-center justify-center rounded-[5px] text-muted-foreground transition-colors hover:bg-muted hover:text-foreground focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-ring/40"
|
|
63
73
|
>
|
|
64
|
-
<XIcon />
|
|
74
|
+
<XIcon className="size-3.5" />
|
|
65
75
|
<span className="sr-only">Close</span>
|
|
66
76
|
</DialogPrimitive.Close>
|
|
67
77
|
)}
|
|
@@ -74,7 +84,7 @@ function DialogHeader({ className, ...props }: React.ComponentProps<'div'>) {
|
|
|
74
84
|
return (
|
|
75
85
|
<div
|
|
76
86
|
data-slot="dialog-header"
|
|
77
|
-
className={cn('flex flex-col gap-
|
|
87
|
+
className={cn('flex flex-col gap-1.5 text-left', className)}
|
|
78
88
|
{...props}
|
|
79
89
|
/>
|
|
80
90
|
);
|
|
@@ -91,7 +101,10 @@ function DialogFooter({
|
|
|
91
101
|
return (
|
|
92
102
|
<div
|
|
93
103
|
data-slot="dialog-footer"
|
|
94
|
-
className={cn(
|
|
104
|
+
className={cn(
|
|
105
|
+
'flex flex-col-reverse gap-2 pt-2 sm:flex-row sm:justify-end sm:gap-1.5',
|
|
106
|
+
className,
|
|
107
|
+
)}
|
|
95
108
|
{...props}
|
|
96
109
|
>
|
|
97
110
|
{children}
|
|
@@ -108,7 +121,10 @@ function DialogTitle({ className, ...props }: React.ComponentProps<typeof Dialog
|
|
|
108
121
|
return (
|
|
109
122
|
<DialogPrimitive.Title
|
|
110
123
|
data-slot="dialog-title"
|
|
111
|
-
className={cn(
|
|
124
|
+
className={cn(
|
|
125
|
+
'font-heading text-[15px] font-semibold leading-tight tracking-tight text-foreground',
|
|
126
|
+
className,
|
|
127
|
+
)}
|
|
112
128
|
{...props}
|
|
113
129
|
/>
|
|
114
130
|
);
|
|
@@ -121,7 +137,7 @@ function DialogDescription({
|
|
|
121
137
|
return (
|
|
122
138
|
<DialogPrimitive.Description
|
|
123
139
|
data-slot="dialog-description"
|
|
124
|
-
className={cn('text-
|
|
140
|
+
className={cn('text-[13px] leading-relaxed text-muted-foreground', className)}
|
|
125
141
|
{...props}
|
|
126
142
|
/>
|
|
127
143
|
);
|
|
@@ -24,7 +24,7 @@ function DropdownMenuTrigger({
|
|
|
24
24
|
|
|
25
25
|
function DropdownMenuContent({
|
|
26
26
|
className,
|
|
27
|
-
sideOffset =
|
|
27
|
+
sideOffset = 6,
|
|
28
28
|
...props
|
|
29
29
|
}: React.ComponentProps<typeof DropdownMenuPrimitive.Content>) {
|
|
30
30
|
return (
|
|
@@ -33,7 +33,11 @@ function DropdownMenuContent({
|
|
|
33
33
|
data-slot="dropdown-menu-content"
|
|
34
34
|
sideOffset={sideOffset}
|
|
35
35
|
className={cn(
|
|
36
|
-
'z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[
|
|
36
|
+
'z-50 max-h-(--radix-dropdown-menu-content-available-height) min-w-[9rem] origin-(--radix-dropdown-menu-content-transform-origin)',
|
|
37
|
+
'overflow-x-hidden overflow-y-auto rounded-[8px] border border-border bg-popover p-1 text-popover-foreground shadow-floating',
|
|
38
|
+
'data-[side=bottom]:slide-in-from-top-1 data-[side=left]:slide-in-from-right-1 data-[side=right]:slide-in-from-left-1 data-[side=top]:slide-in-from-bottom-1',
|
|
39
|
+
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
|
40
|
+
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
|
37
41
|
className,
|
|
38
42
|
)}
|
|
39
43
|
{...props}
|
|
@@ -61,7 +65,12 @@ function DropdownMenuItem({
|
|
|
61
65
|
data-inset={inset}
|
|
62
66
|
data-variant={variant}
|
|
63
67
|
className={cn(
|
|
64
|
-
|
|
68
|
+
'relative flex cursor-default items-center gap-2 rounded-[5px] px-2 py-1.5 text-[12.5px] outline-hidden select-none transition-colors',
|
|
69
|
+
'focus:bg-foreground focus:text-background',
|
|
70
|
+
'data-[active=true]:bg-muted data-[active=true]:text-foreground',
|
|
71
|
+
'data-[disabled]:pointer-events-none data-[disabled]:opacity-45 data-[inset]:pl-8',
|
|
72
|
+
'data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive data-[variant=destructive]:focus:text-white',
|
|
73
|
+
"[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 [&_svg:not([class*='text-'])]:text-current [&_svg]:opacity-80",
|
|
65
74
|
className,
|
|
66
75
|
)}
|
|
67
76
|
{...props}
|
|
@@ -79,7 +88,7 @@ function DropdownMenuCheckboxItem({
|
|
|
79
88
|
<DropdownMenuPrimitive.CheckboxItem
|
|
80
89
|
data-slot="dropdown-menu-checkbox-item"
|
|
81
90
|
className={cn(
|
|
82
|
-
|
|
91
|
+
'relative flex cursor-default items-center gap-2 rounded-[5px] py-1.5 pr-2 pl-8 text-[12.5px] outline-hidden select-none focus:bg-foreground focus:text-background data-[disabled]:pointer-events-none data-[disabled]:opacity-45',
|
|
83
92
|
className,
|
|
84
93
|
)}
|
|
85
94
|
checked={checked}
|
|
@@ -87,7 +96,7 @@ function DropdownMenuCheckboxItem({
|
|
|
87
96
|
>
|
|
88
97
|
<span className="pointer-events-none absolute left-2 flex size-3.5 items-center justify-center">
|
|
89
98
|
<DropdownMenuPrimitive.ItemIndicator>
|
|
90
|
-
<CheckIcon className="size-
|
|
99
|
+
<CheckIcon className="size-3.5" />
|
|
91
100
|
</DropdownMenuPrimitive.ItemIndicator>
|
|
92
101
|
</span>
|
|
93
102
|
{children}
|
|
@@ -110,7 +119,7 @@ function DropdownMenuRadioItem({
|
|
|
110
119
|
<DropdownMenuPrimitive.RadioItem
|
|
111
120
|
data-slot="dropdown-menu-radio-item"
|
|
112
121
|
className={cn(
|
|
113
|
-
|
|
122
|
+
'relative flex cursor-default items-center gap-2 rounded-[5px] py-1.5 pr-2 pl-8 text-[12.5px] outline-hidden select-none focus:bg-foreground focus:text-background data-[disabled]:pointer-events-none data-[disabled]:opacity-45',
|
|
114
123
|
className,
|
|
115
124
|
)}
|
|
116
125
|
{...props}
|
|
@@ -136,7 +145,10 @@ function DropdownMenuLabel({
|
|
|
136
145
|
<DropdownMenuPrimitive.Label
|
|
137
146
|
data-slot="dropdown-menu-label"
|
|
138
147
|
data-inset={inset}
|
|
139
|
-
className={cn(
|
|
148
|
+
className={cn(
|
|
149
|
+
'eyebrow px-2 py-1.5 data-[inset]:pl-8',
|
|
150
|
+
className,
|
|
151
|
+
)}
|
|
140
152
|
{...props}
|
|
141
153
|
/>
|
|
142
154
|
);
|
|
@@ -149,7 +161,7 @@ function DropdownMenuSeparator({
|
|
|
149
161
|
return (
|
|
150
162
|
<DropdownMenuPrimitive.Separator
|
|
151
163
|
data-slot="dropdown-menu-separator"
|
|
152
|
-
className={cn('-mx-1 my-1 h-px bg-
|
|
164
|
+
className={cn('-mx-1 my-1 h-px bg-hairline', className)}
|
|
153
165
|
{...props}
|
|
154
166
|
/>
|
|
155
167
|
);
|
|
@@ -159,7 +171,10 @@ function DropdownMenuShortcut({ className, ...props }: React.ComponentProps<'spa
|
|
|
159
171
|
return (
|
|
160
172
|
<span
|
|
161
173
|
data-slot="dropdown-menu-shortcut"
|
|
162
|
-
className={cn(
|
|
174
|
+
className={cn(
|
|
175
|
+
'ml-auto font-mono text-[10.5px] tracking-[0.06em] text-muted-foreground/80',
|
|
176
|
+
className,
|
|
177
|
+
)}
|
|
163
178
|
{...props}
|
|
164
179
|
/>
|
|
165
180
|
);
|
|
@@ -182,13 +197,13 @@ function DropdownMenuSubTrigger({
|
|
|
182
197
|
data-slot="dropdown-menu-sub-trigger"
|
|
183
198
|
data-inset={inset}
|
|
184
199
|
className={cn(
|
|
185
|
-
|
|
200
|
+
'flex cursor-default items-center gap-2 rounded-[5px] px-2 py-1.5 text-[12.5px] outline-hidden select-none focus:bg-foreground focus:text-background data-[inset]:pl-8 data-[state=open]:bg-muted',
|
|
186
201
|
className,
|
|
187
202
|
)}
|
|
188
203
|
{...props}
|
|
189
204
|
>
|
|
190
205
|
{children}
|
|
191
|
-
<ChevronRightIcon className="ml-auto size-
|
|
206
|
+
<ChevronRightIcon className="ml-auto size-3.5 opacity-60" />
|
|
192
207
|
</DropdownMenuPrimitive.SubTrigger>
|
|
193
208
|
);
|
|
194
209
|
}
|
|
@@ -201,7 +216,9 @@ function DropdownMenuSubContent({
|
|
|
201
216
|
<DropdownMenuPrimitive.SubContent
|
|
202
217
|
data-slot="dropdown-menu-sub-content"
|
|
203
218
|
className={cn(
|
|
204
|
-
'z-50 min-w-[
|
|
219
|
+
'z-50 min-w-[9rem] overflow-hidden rounded-[8px] border border-border bg-popover p-1 text-popover-foreground shadow-floating',
|
|
220
|
+
'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
|
|
221
|
+
'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
|
|
205
222
|
className,
|
|
206
223
|
)}
|
|
207
224
|
{...props}
|