@open-slide/core 0.0.11 → 0.0.13

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 (89) hide show
  1. package/dist/{build-DHiRlpjn.js → build-DC3FTpWO.js} +2 -1
  2. package/dist/cli/bin.js +43 -4
  3. package/dist/{config-LZM903FE.js → config-Cuw0mC5h.js} +592 -63
  4. package/dist/design-BUML7uvZ.js +35 -0
  5. package/dist/{dev-B3JzCYn7.js → dev-BuWsdYvn.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-CIcG-lP3.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/design.ts +64 -0
  70. package/src/app/lib/export-html.ts +10 -1
  71. package/src/app/lib/export-pdf.ts +7 -0
  72. package/src/app/lib/folders.ts +1 -1
  73. package/src/app/lib/inspector/{useEditor.ts → use-editor.ts} +2 -1
  74. package/src/app/lib/sdk.ts +5 -0
  75. package/src/app/lib/slides.ts +1 -1
  76. package/src/app/lib/utils.ts +1 -1
  77. package/src/app/main.tsx +5 -2
  78. package/src/app/routes/{Home.tsx → home.tsx} +266 -97
  79. package/src/app/routes/presenter.tsx +400 -0
  80. package/src/app/routes/slide.tsx +519 -0
  81. package/src/app/styles.css +338 -67
  82. package/src/app/components/PdfProgressToast.tsx +0 -23
  83. package/src/app/components/Player.tsx +0 -100
  84. package/src/app/components/ThumbnailRail.tsx +0 -68
  85. package/src/app/components/inspector/SaveBar.tsx +0 -77
  86. package/src/app/routes/Slide.tsx +0 -478
  87. /package/dist/{config-SXL5qIl6.d.ts → config-DweCbRkQ.d.ts} +0 -0
  88. /package/src/app/lib/inspector/{useComments.ts → use-comments.ts} +0 -0
  89. /package/src/app/lib/{useWheelPageNavigation.ts → use-wheel-page-navigation.ts} +0 -0
@@ -1,21 +1,25 @@
1
- import * as React from "react"
1
+ import * as React from 'react';
2
2
 
3
- import { cn } from "@/lib/utils"
3
+ import { cn } from '@/lib/utils';
4
4
 
5
- function Input({ className, type, ...props }: React.ComponentProps<"input">) {
5
+ function Input({ className, type, ...props }: React.ComponentProps<'input'>) {
6
6
  return (
7
7
  <input
8
8
  type={type}
9
9
  data-slot="input"
10
10
  className={cn(
11
- "h-9 w-full min-w-0 rounded-md border border-input bg-transparent px-3 py-1 text-base shadow-xs transition-[color,box-shadow] outline-none selection:bg-primary selection:text-primary-foreground file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground placeholder:text-muted-foreground disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50 md:text-sm dark:bg-input/30",
12
- "focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50",
13
- "aria-invalid:border-destructive aria-invalid:ring-destructive/20 dark:aria-invalid:ring-destructive/40",
14
- className
11
+ 'h-8 w-full min-w-0 rounded-[5px] border border-border bg-background px-2.5 text-[13px] outline-none',
12
+ 'transition-colors selection:bg-brand-soft selection:text-foreground',
13
+ 'placeholder:text-muted-foreground/70',
14
+ 'focus-visible:border-foreground/40 focus-visible:ring-2 focus-visible:ring-ring/30',
15
+ 'disabled:pointer-events-none disabled:cursor-not-allowed disabled:opacity-50',
16
+ 'aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/25',
17
+ 'file:inline-flex file:h-7 file:border-0 file:bg-transparent file:text-sm file:font-medium file:text-foreground',
18
+ className,
15
19
  )}
16
20
  {...props}
17
21
  />
18
- )
22
+ );
19
23
  }
20
24
 
21
- export { Input }
25
+ export { Input };
@@ -14,7 +14,7 @@ function PopoverTrigger({ ...props }: React.ComponentProps<typeof PopoverPrimiti
14
14
  function PopoverContent({
15
15
  className,
16
16
  align = 'center',
17
- sideOffset = 4,
17
+ sideOffset = 6,
18
18
  ...props
19
19
  }: React.ComponentProps<typeof PopoverPrimitive.Content>) {
20
20
  return (
@@ -24,7 +24,10 @@ function PopoverContent({
24
24
  align={align}
25
25
  sideOffset={sideOffset}
26
26
  className={cn(
27
- 'z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-md border bg-popover p-4 text-popover-foreground shadow-md outline-hidden 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',
27
+ 'z-50 w-72 origin-(--radix-popover-content-transform-origin) rounded-[8px] border border-border bg-popover p-3 text-popover-foreground shadow-floating outline-hidden',
28
+ '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',
29
+ 'data-[state=closed]:animate-out data-[state=closed]:fade-out-0 data-[state=closed]:zoom-out-95',
30
+ 'data-[state=open]:animate-in data-[state=open]:fade-in-0 data-[state=open]:zoom-in-95',
28
31
  className,
29
32
  )}
30
33
  {...props}
@@ -14,14 +14,14 @@ function Progress({
14
14
  <ProgressPrimitive.Root
15
15
  data-slot="progress"
16
16
  className={cn(
17
- "relative h-2 w-full overflow-hidden rounded-full bg-primary/20",
17
+ "relative h-[3px] w-full overflow-hidden rounded-full bg-muted",
18
18
  className
19
19
  )}
20
20
  {...props}
21
21
  >
22
22
  <ProgressPrimitive.Indicator
23
23
  data-slot="progress-indicator"
24
- className="h-full w-full flex-1 bg-primary transition-all"
24
+ className="h-full w-full flex-1 bg-brand transition-all"
25
25
  style={{ transform: `translateX(-${100 - (value || 0)}%)` }}
26
26
  />
27
27
  </ProgressPrimitive.Root>
@@ -37,14 +37,20 @@ function SelectTrigger({
37
37
  data-slot="select-trigger"
38
38
  data-size={size}
39
39
  className={cn(
40
- "flex w-fit items-center justify-between gap-2 rounded-md border border-input bg-transparent px-3 py-2 text-sm whitespace-nowrap shadow-xs transition-[color,box-shadow] outline-none focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[placeholder]:text-muted-foreground data-[size=default]:h-9 data-[size=sm]:h-8 *:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2 dark:bg-input/30 dark:hover:bg-input/50 dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4 [&_svg:not([class*='text-'])]:text-muted-foreground",
40
+ "flex w-fit items-center justify-between gap-2 rounded-[5px] border border-border bg-background px-2.5 text-[13px] whitespace-nowrap outline-none transition-colors",
41
+ "hover:border-foreground/25 focus-visible:border-foreground/40 focus-visible:ring-2 focus-visible:ring-ring/30",
42
+ "disabled:cursor-not-allowed disabled:opacity-50",
43
+ "aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/25",
44
+ "data-[placeholder]:text-muted-foreground/70 data-[size=default]:h-8 data-[size=sm]:h-7",
45
+ "*:data-[slot=select-value]:line-clamp-1 *:data-[slot=select-value]:flex *:data-[slot=select-value]:items-center *:data-[slot=select-value]:gap-2",
46
+ "[&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 [&_svg:not([class*='text-'])]:text-muted-foreground",
41
47
  className
42
48
  )}
43
49
  {...props}
44
50
  >
45
51
  {children}
46
52
  <SelectPrimitive.Icon asChild>
47
- <ChevronDownIcon className="size-4 opacity-50" />
53
+ <ChevronDownIcon className="size-3.5 opacity-50" />
48
54
  </SelectPrimitive.Icon>
49
55
  </SelectPrimitive.Trigger>
50
56
  )
@@ -62,7 +68,7 @@ function SelectContent({
62
68
  <SelectPrimitive.Content
63
69
  data-slot="select-content"
64
70
  className={cn(
65
- "relative z-50 max-h-(--radix-select-content-available-height) min-w-[8rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-md border bg-popover 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",
71
+ "relative z-50 max-h-(--radix-select-content-available-height) min-w-[9rem] origin-(--radix-select-content-transform-origin) overflow-x-hidden overflow-y-auto rounded-[8px] border border-border bg-popover text-popover-foreground shadow-floating 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 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",
66
72
  position === "popper" &&
67
73
  "data-[side=bottom]:translate-y-1 data-[side=left]:-translate-x-1 data-[side=right]:translate-x-1 data-[side=top]:-translate-y-1",
68
74
  className
@@ -109,7 +115,7 @@ function SelectItem({
109
115
  <SelectPrimitive.Item
110
116
  data-slot="select-item"
111
117
  className={cn(
112
- "relative flex w-full cursor-default items-center gap-2 rounded-sm py-1.5 pr-8 pl-2 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 [&_svg:not([class*='text-'])]:text-muted-foreground *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
118
+ "relative flex w-full cursor-default items-center gap-2 rounded-[5px] py-1.5 pr-8 pl-2 text-[12.5px] outline-hidden select-none focus:bg-foreground focus:text-background data-[disabled]:pointer-events-none data-[disabled]:opacity-45 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5 [&_svg:not([class*='text-'])]:text-current *:[span]:last:flex *:[span]:last:items-center *:[span]:last:gap-2",
113
119
  className
114
120
  )}
115
121
  {...props}
@@ -119,7 +125,7 @@ function SelectItem({
119
125
  className="absolute right-2 flex size-3.5 items-center justify-center"
120
126
  >
121
127
  <SelectPrimitive.ItemIndicator>
122
- <CheckIcon className="size-4" />
128
+ <CheckIcon className="size-3.5" />
123
129
  </SelectPrimitive.ItemIndicator>
124
130
  </span>
125
131
  <SelectPrimitive.ItemText>{children}</SelectPrimitive.ItemText>
@@ -17,7 +17,7 @@ function Separator({
17
17
  decorative={decorative}
18
18
  orientation={orientation}
19
19
  className={cn(
20
- 'shrink-0 bg-border data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch',
20
+ 'shrink-0 bg-hairline data-horizontal:h-px data-horizontal:w-full data-vertical:w-px data-vertical:self-stretch',
21
21
  className,
22
22
  )}
23
23
  {...props}
@@ -29,7 +29,7 @@ function Slider({
29
29
  min={min}
30
30
  max={max}
31
31
  className={cn(
32
- "relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
32
+ "group/slider relative flex w-full touch-none items-center select-none data-[disabled]:opacity-50 data-[orientation=vertical]:h-full data-[orientation=vertical]:min-h-44 data-[orientation=vertical]:w-auto data-[orientation=vertical]:flex-col",
33
33
  className
34
34
  )}
35
35
  {...props}
@@ -37,13 +37,13 @@ function Slider({
37
37
  <SliderPrimitive.Track
38
38
  data-slot="slider-track"
39
39
  className={cn(
40
- "relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-1.5 data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-1.5"
40
+ "relative grow overflow-hidden rounded-full bg-muted data-[orientation=horizontal]:h-[3px] data-[orientation=horizontal]:w-full data-[orientation=vertical]:h-full data-[orientation=vertical]:w-[3px]"
41
41
  )}
42
42
  >
43
43
  <SliderPrimitive.Range
44
44
  data-slot="slider-range"
45
45
  className={cn(
46
- "absolute bg-primary data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
46
+ "absolute bg-foreground data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
47
47
  )}
48
48
  />
49
49
  </SliderPrimitive.Track>
@@ -51,7 +51,7 @@ function Slider({
51
51
  <SliderPrimitive.Thumb
52
52
  data-slot="slider-thumb"
53
53
  key={index}
54
- className="block size-4 shrink-0 rounded-full border border-primary bg-white shadow-sm ring-ring/50 transition-[color,box-shadow] hover:ring-4 focus-visible:ring-4 focus-visible:outline-hidden disabled:pointer-events-none disabled:opacity-50"
54
+ className="block size-3.5 shrink-0 rounded-full border border-foreground bg-card shadow-edge transition-transform hover:scale-110 focus-visible:scale-110 focus-visible:outline-2 focus-visible:outline-offset-2 focus-visible:outline-ring/50 disabled:pointer-events-none disabled:opacity-50"
55
55
  />
56
56
  ))}
57
57
  </SliderPrimitive.Root>
@@ -27,9 +27,19 @@ const Toaster = ({ ...props }: ToasterProps) => {
27
27
  "--normal-bg": "var(--popover)",
28
28
  "--normal-text": "var(--popover-foreground)",
29
29
  "--normal-border": "var(--border)",
30
- "--border-radius": "var(--radius)",
30
+ "--border-radius": "8px",
31
+ "--font-family":
32
+ "Geist Variable, -apple-system, BlinkMacSystemFont, sans-serif",
31
33
  } as React.CSSProperties
32
34
  }
35
+ toastOptions={{
36
+ classNames: {
37
+ toast:
38
+ "!font-sans !text-[12.5px] !shadow-floating !border-border !rounded-[8px]",
39
+ title: "!font-medium !text-[12.5px] !tracking-tight",
40
+ description: "!text-[12px] !text-muted-foreground",
41
+ },
42
+ }}
33
43
  {...props}
34
44
  />
35
45
  )
@@ -21,11 +21,11 @@ function Tabs({
21
21
  }
22
22
 
23
23
  const tabsListVariants = cva(
24
- 'group/tabs-list inline-flex w-fit items-center justify-center rounded-lg p-[3px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-9 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none',
24
+ 'group/tabs-list inline-flex w-fit items-center justify-center rounded-[6px] p-[2px] text-muted-foreground group-data-[orientation=horizontal]/tabs:h-7 group-data-[orientation=vertical]/tabs:h-fit group-data-[orientation=vertical]/tabs:flex-col data-[variant=line]:rounded-none',
25
25
  {
26
26
  variants: {
27
27
  variant: {
28
- default: 'bg-muted',
28
+ default: 'bg-muted/70 ring-1 ring-inset ring-border/60',
29
29
  line: 'gap-1 bg-transparent',
30
30
  },
31
31
  },
@@ -55,10 +55,10 @@ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPr
55
55
  <TabsPrimitive.Trigger
56
56
  data-slot="tabs-trigger"
57
57
  className={cn(
58
- "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-md border border-transparent px-2 py-1 text-sm font-medium whitespace-nowrap text-foreground/60 transition-all group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 focus-visible:outline-1 focus-visible:outline-ring disabled:pointer-events-none disabled:opacity-50 group-data-[variant=default]/tabs-list:data-[state=active]:shadow-sm group-data-[variant=line]/tabs-list:data-[state=active]:shadow-none dark:text-muted-foreground dark:hover:text-foreground [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
59
- 'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:border-transparent dark:group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent',
60
- 'data-[state=active]:bg-background data-[state=active]:text-foreground dark:data-[state=active]:border-input dark:data-[state=active]:bg-input/30 dark:data-[state=active]:text-foreground',
61
- 'after:absolute after:bg-foreground after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:bottom-[-5px] group-data-[orientation=horizontal]/tabs:after:h-0.5 group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100',
58
+ "relative inline-flex h-[calc(100%-1px)] flex-1 items-center justify-center gap-1.5 rounded-[5px] border border-transparent px-2.5 text-[12px] font-medium whitespace-nowrap text-foreground/55 transition-colors group-data-[orientation=vertical]/tabs:w-full group-data-[orientation=vertical]/tabs:justify-start hover:text-foreground focus-visible:outline-2 focus-visible:outline-offset-1 focus-visible:outline-ring/50 disabled:pointer-events-none disabled:opacity-50 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
59
+ 'group-data-[variant=line]/tabs-list:bg-transparent group-data-[variant=line]/tabs-list:data-[state=active]:bg-transparent',
60
+ 'data-[state=active]:bg-card data-[state=active]:text-foreground data-[state=active]:shadow-edge dark:data-[state=active]:bg-foreground/10',
61
+ 'after:absolute after:bg-brand after:opacity-0 after:transition-opacity group-data-[orientation=horizontal]/tabs:after:inset-x-0 group-data-[orientation=horizontal]/tabs:after:-bottom-[6px] group-data-[orientation=horizontal]/tabs:after:h-[2px] group-data-[orientation=vertical]/tabs:after:inset-y-0 group-data-[orientation=vertical]/tabs:after:-right-1 group-data-[orientation=vertical]/tabs:after:w-0.5 group-data-[variant=line]/tabs-list:data-[state=active]:after:opacity-100',
62
62
  className,
63
63
  )}
64
64
  {...props}
@@ -1,18 +1,22 @@
1
- import * as React from "react"
1
+ import * as React from 'react';
2
2
 
3
- import { cn } from "@/lib/utils"
3
+ import { cn } from '@/lib/utils';
4
4
 
5
- function Textarea({ className, ...props }: React.ComponentProps<"textarea">) {
5
+ function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
6
6
  return (
7
7
  <textarea
8
8
  data-slot="textarea"
9
9
  className={cn(
10
- "flex field-sizing-content min-h-16 w-full rounded-md border border-input bg-transparent px-3 py-2 text-base shadow-xs transition-[color,box-shadow] outline-none placeholder:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:cursor-not-allowed disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 md:text-sm dark:bg-input/30 dark:aria-invalid:ring-destructive/40",
11
- className
10
+ 'flex field-sizing-content min-h-16 w-full rounded-[6px] border border-border bg-background px-2.5 py-2 text-[13px] leading-relaxed outline-none',
11
+ 'transition-colors placeholder:text-muted-foreground/70',
12
+ 'focus-visible:border-foreground/40 focus-visible:ring-2 focus-visible:ring-ring/30',
13
+ 'disabled:cursor-not-allowed disabled:opacity-50',
14
+ 'aria-invalid:border-destructive aria-invalid:ring-2 aria-invalid:ring-destructive/25',
15
+ className,
12
16
  )}
13
17
  {...props}
14
18
  />
15
- )
19
+ );
16
20
  }
17
21
 
18
- export { Textarea }
22
+ export { Textarea };
@@ -36,7 +36,7 @@ function ToggleGroup({
36
36
  data-spacing={spacing}
37
37
  style={{ "--gap": spacing } as React.CSSProperties}
38
38
  className={cn(
39
- "group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-md data-[spacing=default]:data-[variant=outline]:shadow-xs",
39
+ "group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-[5px]",
40
40
  className
41
41
  )}
42
42
  {...props}
@@ -70,7 +70,7 @@ function ToggleGroupItem({
70
70
  size: context.size || size,
71
71
  }),
72
72
  "w-auto min-w-0 shrink-0 px-3 focus:z-10 focus-visible:z-10",
73
- "data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-md data-[spacing=0]:last:rounded-r-md data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
73
+ "data-[spacing=0]:rounded-none data-[spacing=0]:shadow-none data-[spacing=0]:first:rounded-l-[5px] data-[spacing=0]:last:rounded-r-[5px] data-[spacing=0]:data-[variant=outline]:border-l-0 data-[spacing=0]:data-[variant=outline]:first:border-l",
74
74
  className
75
75
  )}
76
76
  {...props}
@@ -5,18 +5,18 @@ import { Toggle as TogglePrimitive } from "radix-ui"
5
5
  import { cn } from "@/lib/utils"
6
6
 
7
7
  const toggleVariants = cva(
8
- "inline-flex items-center justify-center gap-2 rounded-md text-sm font-medium whitespace-nowrap transition-[color,box-shadow] outline-none hover:bg-muted hover:text-muted-foreground focus-visible:border-ring focus-visible:ring-[3px] focus-visible:ring-ring/50 disabled:pointer-events-none disabled:opacity-50 aria-invalid:border-destructive aria-invalid:ring-destructive/20 data-[state=on]:bg-accent data-[state=on]:text-accent-foreground dark:aria-invalid:ring-destructive/40 [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-4",
8
+ "inline-flex items-center justify-center gap-2 rounded-[5px] text-[12px] font-medium whitespace-nowrap outline-none transition-colors hover:bg-muted hover:text-foreground focus-visible:ring-2 focus-visible:ring-ring/30 disabled:pointer-events-none disabled:opacity-50 data-[state=on]:bg-foreground data-[state=on]:text-background [&_svg]:pointer-events-none [&_svg]:shrink-0 [&_svg:not([class*='size-'])]:size-3.5",
9
9
  {
10
10
  variants: {
11
11
  variant: {
12
- default: "bg-transparent",
12
+ default: "bg-transparent text-foreground/70",
13
13
  outline:
14
- "border border-input bg-transparent shadow-xs hover:bg-accent hover:text-accent-foreground",
14
+ "border border-border bg-card text-foreground/75 hover:border-foreground/20 data-[state=on]:border-foreground",
15
15
  },
16
16
  size: {
17
- default: "h-9 min-w-9 px-2",
18
- sm: "h-8 min-w-8 px-1.5",
19
- lg: "h-10 min-w-10 px-2.5",
17
+ default: "h-8 min-w-8 px-2",
18
+ sm: "h-7 min-w-7 px-1.5",
19
+ lg: "h-9 min-w-9 px-2.5",
20
20
  },
21
21
  },
22
22
  defaultVariants: {
@@ -32,10 +32,13 @@ function TooltipContent({
32
32
  className,
33
33
  sideOffset = 0,
34
34
  children,
35
+ container,
35
36
  ...props
36
- }: React.ComponentProps<typeof TooltipPrimitive.Content>) {
37
+ }: React.ComponentProps<typeof TooltipPrimitive.Content> & {
38
+ container?: React.ComponentProps<typeof TooltipPrimitive.Portal>['container']
39
+ }) {
37
40
  return (
38
- <TooltipPrimitive.Portal>
41
+ <TooltipPrimitive.Portal container={container}>
39
42
  <TooltipPrimitive.Content
40
43
  data-slot="tooltip-content"
41
44
  sideOffset={sideOffset}
@@ -0,0 +1,64 @@
1
+ export type DesignPalette = {
2
+ bg: string;
3
+ text: string;
4
+ accent: string;
5
+ };
6
+
7
+ export type DesignFonts = {
8
+ display: string;
9
+ body: string;
10
+ };
11
+
12
+ export type DesignTypeScale = {
13
+ hero: number;
14
+ body: number;
15
+ };
16
+
17
+ export type DesignRadius = {
18
+ md: number;
19
+ };
20
+
21
+ export type DesignSystem = {
22
+ palette: DesignPalette;
23
+ fonts: DesignFonts;
24
+ typeScale: DesignTypeScale;
25
+ radius: DesignRadius;
26
+ };
27
+
28
+ export function designToCssVars(d: DesignSystem): Record<string, string> {
29
+ return {
30
+ '--osd-bg': d.palette.bg,
31
+ '--osd-text': d.palette.text,
32
+ '--osd-accent': d.palette.accent,
33
+ '--osd-font-display': d.fonts.display,
34
+ '--osd-font-body': d.fonts.body,
35
+ '--osd-size-hero': `${d.typeScale.hero}px`,
36
+ '--osd-size-body': `${d.typeScale.body}px`,
37
+ '--osd-radius-md': `${d.radius.md}px`,
38
+ };
39
+ }
40
+
41
+ export function cssVarsToString(vars: Record<string, string>): string {
42
+ return Object.entries(vars)
43
+ .map(([k, v]) => ` ${k}: ${v};`)
44
+ .join('\n');
45
+ }
46
+
47
+ export const defaultDesign: DesignSystem = {
48
+ palette: {
49
+ bg: '#f7f5f0',
50
+ text: '#1a1814',
51
+ accent: '#6d4cff',
52
+ },
53
+ fonts: {
54
+ display: 'Georgia, "Times New Roman", serif',
55
+ body: '-apple-system, BlinkMacSystemFont, "Inter", system-ui, sans-serif',
56
+ },
57
+ typeScale: {
58
+ hero: 168,
59
+ body: 36,
60
+ },
61
+ radius: {
62
+ md: 12,
63
+ },
64
+ };
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { createElement } from 'react';
7
7
  import { createRoot } from 'react-dom/client';
8
+ import { designToCssVars } from './design';
8
9
  import type { SlideModule } from './sdk';
9
10
 
10
11
  type AssetEntry = { name: string; bytes: Uint8Array };
@@ -51,6 +52,7 @@ export async function exportSlideAsHtml(slide: SlideModule, slideId: string): Pr
51
52
  pagesHtml: rewrittenPages,
52
53
  bundledCss: rewrittenCss,
53
54
  externalLinks,
55
+ design: slide.design,
54
56
  });
55
57
 
56
58
  const htmlBytes = new TextEncoder().encode(html);
@@ -237,6 +239,7 @@ function buildHtml(opts: {
237
239
  pagesHtml: string[];
238
240
  bundledCss: string;
239
241
  externalLinks: string;
242
+ design: SlideModule['design'];
240
243
  }): string {
241
244
  const pagesMarkup = opts.pagesHtml
242
245
  .map(
@@ -244,6 +247,12 @@ function buildHtml(opts: {
244
247
  )
245
248
  .join('');
246
249
 
250
+ const frameStyle = opts.design
251
+ ? Object.entries(designToCssVars(opts.design))
252
+ .map(([k, v]) => `${k}: ${v};`)
253
+ .join(' ')
254
+ : '';
255
+
247
256
  return `<!doctype html>
248
257
  <html lang="en">
249
258
  <head>
@@ -262,7 +271,7 @@ html, body { margin: 0; height: 100%; background: #000; overflow: hidden; font-f
262
271
  <style>${opts.bundledCss}</style>
263
272
  </head>
264
273
  <body>
265
- <div class="os-stage"><div class="os-frame" id="os-frame">${pagesMarkup}</div></div>
274
+ <div class="os-stage"><div class="os-frame" id="os-frame" data-osd-canvas${frameStyle ? ` style="${escapeAttr(frameStyle)}"` : ''}>${pagesMarkup}</div></div>
266
275
  <div class="os-counter"><span id="os-cur">1</span> / <span id="os-total">${opts.pagesHtml.length}</span></div>
267
276
  <script>
268
277
  (function () {
@@ -5,6 +5,7 @@
5
5
 
6
6
  import { createElement } from 'react';
7
7
  import { createRoot, type Root } from 'react-dom/client';
8
+ import { designToCssVars } from './design';
8
9
  import { isFrameAnimationSettled, waitForDataWaitfor, waitForFonts } from './print-ready';
9
10
  import type { SlideModule } from './sdk';
10
11
 
@@ -103,13 +104,19 @@ export async function exportSlideAsPdf(
103
104
 
104
105
  onProgress?.({ phase: 'processing', current: 0, total, percent: 0 });
105
106
 
107
+ const designVars = slide.design ? designToCssVars(slide.design) : null;
108
+
106
109
  const reactRoots: Root[] = [];
107
110
  const frames: HTMLElement[] = [];
108
111
  for (const Page of pages) {
109
112
  const host = document.createElement('div');
110
113
  host.className = 'os-print-frame';
114
+ host.setAttribute('data-osd-canvas', '');
111
115
  host.style.width = '1920px';
112
116
  host.style.height = '1080px';
117
+ if (designVars) {
118
+ for (const [k, v] of Object.entries(designVars)) host.style.setProperty(k, v);
119
+ }
113
120
  const inner = document.createElement('div');
114
121
  inner.className = 'os-print-supersample';
115
122
  inner.style.width = '1920px';
@@ -1,5 +1,5 @@
1
- import { useCallback, useEffect, useState } from 'react';
2
1
  import buildManifest from 'virtual:open-slide/folders';
2
+ import { useCallback, useEffect, useState } from 'react';
3
3
  import type { Folder, FolderIcon, FoldersManifest } from './sdk';
4
4
 
5
5
  const EMPTY: FoldersManifest = { folders: [], assignments: {} };
@@ -3,7 +3,8 @@ import { useCallback } from 'react';
3
3
  export type EditOp =
4
4
  | { kind: 'set-style'; key: string; value: string | null }
5
5
  | { kind: 'set-text'; value: string }
6
- | { kind: 'set-attr-asset'; attr: string; assetPath: string; previewUrl: string };
6
+ | { kind: 'set-attr-asset'; attr: string; assetPath: string; previewUrl: string }
7
+ | { kind: 'replace-placeholder-with-image'; assetPath: string };
7
8
 
8
9
  export type Edit = { line: number; column: number; ops: EditOp[] };
9
10
 
@@ -1,4 +1,5 @@
1
1
  import type { ComponentType } from 'react';
2
+ import type { DesignSystem } from './design.ts';
2
3
 
3
4
  export type Page = ComponentType;
4
5
 
@@ -9,6 +10,10 @@ export type SlideMeta = {
9
10
  export type SlideModule = {
10
11
  default: Page[];
11
12
  meta?: SlideMeta;
13
+ design?: DesignSystem;
14
+ // Index-aligned with `default`. Each entry is the speaker note for the
15
+ // page at the same position. Used by Presenter View only.
16
+ notes?: (string | undefined)[];
12
17
  };
13
18
 
14
19
  export type FolderIcon = { type: 'emoji'; value: string } | { type: 'color'; value: string };
@@ -1,5 +1,5 @@
1
- import type { SlideModule } from './sdk';
2
1
  import { slideIds as ids, loadSlide as load } from 'virtual:open-slide/slides';
2
+ import type { SlideModule } from './sdk';
3
3
 
4
4
  export const slideIds: string[] = ids;
5
5
 
@@ -1,4 +1,4 @@
1
- import { clsx, type ClassValue } from 'clsx';
1
+ import { type ClassValue, clsx } from 'clsx';
2
2
  import { twMerge } from 'tailwind-merge';
3
3
 
4
4
  export function cn(...inputs: ClassValue[]) {
package/src/app/main.tsx CHANGED
@@ -1,11 +1,14 @@
1
+ import { ThemeProvider } from 'next-themes';
1
2
  import { StrictMode } from 'react';
2
3
  import { createRoot } from 'react-dom/client';
3
- import { App } from './App';
4
+ import { App } from './app';
4
5
  import './styles.css';
5
6
 
6
7
  // biome-ignore lint/style/noNonNullAssertion: #root is guaranteed by index.html
7
8
  createRoot(document.getElementById('root')!).render(
8
9
  <StrictMode>
9
- <App />
10
+ <ThemeProvider attribute="class" defaultTheme="system" enableSystem disableTransitionOnChange>
11
+ <App />
12
+ </ThemeProvider>
10
13
  </StrictMode>,
11
14
  );