@open-slide/core 0.0.11 → 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.
Files changed (88) hide show
  1. package/dist/{build-DHiRlpjn.js → build-aiY_8kwE.js} +2 -1
  2. package/dist/cli/bin.js +43 -4
  3. package/dist/{config-LZM903FE.js → config-CVqRAagl.js} +592 -63
  4. package/dist/design-CROQh0AA.js +35 -0
  5. package/dist/{dev-B3JzCYn7.js → dev-R2we2iaF.js} +2 -1
  6. package/dist/index.d.ts +55 -4
  7. package/dist/index.js +110 -1
  8. package/dist/{preview-UikovHEt.js → preview-CU4zSyGp.js} +2 -1
  9. package/dist/sync-3oqN1WyK.js +139 -0
  10. package/dist/sync-B4eLo2H6.js +3 -0
  11. package/dist/vite/index.d.ts +1 -1
  12. package/dist/vite/index.js +2 -1
  13. package/package.json +2 -1
  14. package/skills/apply-comments/SKILL.md +83 -0
  15. package/skills/create-slide/SKILL.md +81 -0
  16. package/skills/create-theme/SKILL.md +194 -0
  17. package/skills/slide-authoring/SKILL.md +288 -0
  18. package/src/app/{App.tsx → app.tsx} +8 -6
  19. package/src/app/components/{AssetView.tsx → asset-view.tsx} +41 -33
  20. package/src/app/components/{ClickNavZones.tsx → click-nav-zones.tsx} +1 -1
  21. package/src/app/components/history-provider.tsx +120 -0
  22. package/src/app/components/image-placeholder.tsx +121 -0
  23. package/src/app/components/inspector/{CommentWidget.tsx → comment-widget.tsx} +1 -1
  24. package/src/app/components/inspector/{InspectOverlay.tsx → inspect-overlay.tsx} +1 -1
  25. package/src/app/components/inspector/{InspectorPanel.tsx → inspector-panel.tsx} +164 -212
  26. package/src/app/components/inspector/{InspectorProvider.tsx → inspector-provider.tsx} +186 -18
  27. package/src/app/components/inspector/save-bar.tsx +47 -0
  28. package/src/app/components/panel/panel-fields.tsx +60 -0
  29. package/src/app/components/panel/panel-shell.tsx +78 -0
  30. package/src/app/components/panel/save-card.tsx +139 -0
  31. package/src/app/components/pdf-progress-toast.tsx +25 -0
  32. package/src/app/components/player.tsx +341 -0
  33. package/src/app/components/present/blackout-overlay.tsx +18 -0
  34. package/src/app/components/present/control-bar.tsx +204 -0
  35. package/src/app/components/present/help-overlay.tsx +56 -0
  36. package/src/app/components/present/jump-input.tsx +74 -0
  37. package/src/app/components/present/laser-pointer.tsx +40 -0
  38. package/src/app/components/present/overview-grid.tsx +184 -0
  39. package/src/app/components/present/progress-bar.tsx +26 -0
  40. package/src/app/components/present/use-idle.ts +44 -0
  41. package/src/app/components/present/use-pointer-near-bottom.ts +34 -0
  42. package/src/app/components/present/use-presenter-channel.ts +71 -0
  43. package/src/app/components/present/use-touch-swipe.ts +63 -0
  44. package/src/app/components/sidebar/{FolderItem.tsx → folder-item.tsx} +62 -27
  45. package/src/app/components/sidebar/{IconPicker.tsx → icon-picker.tsx} +13 -10
  46. package/src/app/components/sidebar/{Sidebar.tsx → sidebar.tsx} +40 -34
  47. package/src/app/components/{SlideCanvas.tsx → slide-canvas.tsx} +35 -10
  48. package/src/app/components/style-panel/design-provider.tsx +139 -0
  49. package/src/app/components/style-panel/style-panel.tsx +326 -0
  50. package/src/app/components/style-panel/use-design.ts +112 -0
  51. package/src/app/components/theme-toggle.tsx +57 -0
  52. package/src/app/components/thumbnail-rail.tsx +151 -0
  53. package/src/app/components/ui/button.tsx +51 -19
  54. package/src/app/components/ui/card.tsx +1 -1
  55. package/src/app/components/ui/dialog.tsx +25 -9
  56. package/src/app/components/ui/dropdown-menu.tsx +29 -12
  57. package/src/app/components/ui/input.tsx +13 -9
  58. package/src/app/components/ui/popover.tsx +5 -2
  59. package/src/app/components/ui/progress.tsx +2 -2
  60. package/src/app/components/ui/select.tsx +11 -5
  61. package/src/app/components/ui/separator.tsx +1 -1
  62. package/src/app/components/ui/slider.tsx +4 -4
  63. package/src/app/components/ui/sonner.tsx +11 -1
  64. package/src/app/components/ui/tabs.tsx +6 -6
  65. package/src/app/components/ui/textarea.tsx +11 -7
  66. package/src/app/components/ui/toggle-group.tsx +2 -2
  67. package/src/app/components/ui/toggle.tsx +6 -6
  68. package/src/app/components/ui/tooltip.tsx +5 -2
  69. package/src/app/lib/export-html.ts +10 -1
  70. package/src/app/lib/export-pdf.ts +7 -0
  71. package/src/app/lib/folders.ts +1 -1
  72. package/src/app/lib/inspector/{useEditor.ts → use-editor.ts} +2 -1
  73. package/src/app/lib/sdk.ts +5 -0
  74. package/src/app/lib/slides.ts +1 -1
  75. package/src/app/lib/utils.ts +1 -1
  76. package/src/app/main.tsx +5 -2
  77. package/src/app/routes/{Home.tsx → home.tsx} +266 -97
  78. package/src/app/routes/presenter.tsx +400 -0
  79. package/src/app/routes/slide.tsx +519 -0
  80. package/src/app/styles.css +338 -67
  81. package/src/app/components/PdfProgressToast.tsx +0 -23
  82. package/src/app/components/Player.tsx +0 -100
  83. package/src/app/components/ThumbnailRail.tsx +0 -68
  84. package/src/app/components/inspector/SaveBar.tsx +0 -77
  85. package/src/app/routes/Slide.tsx +0 -478
  86. /package/dist/{config-SXL5qIl6.d.ts → config-DweCbRkQ.d.ts} +0 -0
  87. /package/src/app/lib/inspector/{useComments.ts → use-comments.ts} +0 -0
  88. /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
- "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",
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: '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',
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 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',
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
- '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',
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
- "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',
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-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',
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-black/50 data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=open]:animate-in data-[state=open]:fade-in-0',
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
- 'fixed top-[50%] left-[50%] z-50 grid w-full max-w-[calc(100%-2rem)] translate-x-[-50%] translate-y-[-50%] gap-4 rounded-lg border bg-background p-6 shadow-lg duration-200 outline-none data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95 sm:max-w-lg',
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
- className="absolute top-4 right-4 rounded-xs opacity-70 ring-offset-background transition-opacity hover:opacity-100 focus:ring-2 focus:ring-ring focus:ring-offset-2 focus:outline-hidden disabled:pointer-events-none data-[state=open]:bg-accent data-[state=open]:text-muted-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4"
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-2 text-center sm:text-left', className)}
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('flex flex-col-reverse gap-2 sm:flex-row sm:justify-end', className)}
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('text-lg leading-none font-semibold', className)}
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-sm text-muted-foreground', className)}
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 = 4,
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-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover p-1 text-popover-foreground shadow-md data-[side=bottom]:slide-in-from-top-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-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
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
- "relative flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 data-[inset]:pl-8 data-[variant=destructive]:text-destructive data-[variant=destructive]:focus:bg-destructive/10 data-[variant=destructive]:focus:text-destructive dark:data-[variant=destructive]:focus:bg-destructive/20 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground data-[variant=destructive]:*:[svg]:text-destructive!",
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
- "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
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-4" />
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
- "relative flex cursor-default items-center gap-2 rounded-sm py-1.5 pr-2 pl-8 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[disabled]:pointer-events-none data-[disabled]:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
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('px-2 py-1.5 text-sm font-medium data-[inset]:pl-8', className)}
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-border', className)}
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('ml-auto text-xs tracking-widest text-muted-foreground', className)}
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
- "flex cursor-default items-center gap-2 rounded-sm px-2 py-1.5 text-sm outline-hidden select-none focus:bg-accent focus:text-accent-foreground data-[inset]:pl-8 data-[state=open]:bg-accent data-[state=open]:text-accent-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
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-4" />
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-[8rem] origin-(--radix-dropdown-menu-content-transform-origin) overflow-hidden rounded-md border bg-popover p-1 text-popover-foreground shadow-lg data-[side=bottom]:slide-in-from-top-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-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95 data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
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}