@open-aippt/core 1.13.2

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 (142) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +98 -0
  3. package/bin.js +2 -0
  4. package/dist/build-DxTqmvsO.js +17 -0
  5. package/dist/cli/bin.d.ts +1 -0
  6. package/dist/cli/bin.js +86 -0
  7. package/dist/config-CjzqjrEA.js +4280 -0
  8. package/dist/config-DIC-yVPp.d.ts +23 -0
  9. package/dist/design-cpzS8aud.js +35 -0
  10. package/dist/dev-BYuTeJbA.js +20 -0
  11. package/dist/format-BCeKbTOM.js +1605 -0
  12. package/dist/index.d.ts +134 -0
  13. package/dist/index.js +467 -0
  14. package/dist/locale/index.d.ts +24 -0
  15. package/dist/locale/index.js +3 -0
  16. package/dist/preview-DlQvnJPq.js +18 -0
  17. package/dist/sync-BPZ0m27m.js +139 -0
  18. package/dist/sync-EsYusbbL.js +3 -0
  19. package/dist/types-CHmFPIG_.d.ts +430 -0
  20. package/dist/vite/index.d.ts +14 -0
  21. package/dist/vite/index.js +4 -0
  22. package/env.d.ts +59 -0
  23. package/package.json +103 -0
  24. package/skills/apply-comments/SKILL.md +83 -0
  25. package/skills/create-slide/SKILL.md +91 -0
  26. package/skills/create-theme/SKILL.md +250 -0
  27. package/skills/current-slide/SKILL.md +110 -0
  28. package/skills/slide-authoring/SKILL.md +625 -0
  29. package/src/app/app.tsx +47 -0
  30. package/src/app/components/asset-view.tsx +966 -0
  31. package/src/app/components/history-provider.tsx +120 -0
  32. package/src/app/components/image-placeholder.tsx +243 -0
  33. package/src/app/components/inspector/asset-picker-dialog.tsx +196 -0
  34. package/src/app/components/inspector/comment-widget.tsx +93 -0
  35. package/src/app/components/inspector/image-crop-dialog.tsx +212 -0
  36. package/src/app/components/inspector/inspect-overlay.tsx +387 -0
  37. package/src/app/components/inspector/inspector-panel.tsx +1115 -0
  38. package/src/app/components/inspector/inspector-provider.tsx +1218 -0
  39. package/src/app/components/inspector/save-bar.tsx +48 -0
  40. package/src/app/components/language-toggle.tsx +39 -0
  41. package/src/app/components/notes-drawer.tsx +120 -0
  42. package/src/app/components/overview-grid.tsx +363 -0
  43. package/src/app/components/panel/panel-fields.tsx +60 -0
  44. package/src/app/components/panel/panel-shell.tsx +80 -0
  45. package/src/app/components/panel/save-card.tsx +142 -0
  46. package/src/app/components/pdf-progress-toast.tsx +32 -0
  47. package/src/app/components/player.tsx +466 -0
  48. package/src/app/components/pptx-progress-toast.tsx +32 -0
  49. package/src/app/components/present/blackout-overlay.tsx +18 -0
  50. package/src/app/components/present/control-bar.tsx +315 -0
  51. package/src/app/components/present/help-overlay.tsx +57 -0
  52. package/src/app/components/present/jump-input.tsx +74 -0
  53. package/src/app/components/present/laser-pointer.tsx +39 -0
  54. package/src/app/components/present/progress-bar.tsx +26 -0
  55. package/src/app/components/present/use-idle.ts +46 -0
  56. package/src/app/components/present/use-pointer-near-bottom.ts +34 -0
  57. package/src/app/components/present/use-presenter-channel.ts +66 -0
  58. package/src/app/components/present/use-touch-swipe.ts +66 -0
  59. package/src/app/components/shared-element.tsx +48 -0
  60. package/src/app/components/sidebar/folder-item.tsx +258 -0
  61. package/src/app/components/sidebar/icon-picker.tsx +61 -0
  62. package/src/app/components/sidebar/mobile-pill.tsx +34 -0
  63. package/src/app/components/sidebar/sidebar-footer.tsx +105 -0
  64. package/src/app/components/sidebar/sidebar.tsx +284 -0
  65. package/src/app/components/slide-canvas.tsx +102 -0
  66. package/src/app/components/slide-transition-layer.tsx +844 -0
  67. package/src/app/components/style-panel/design-provider.tsx +148 -0
  68. package/src/app/components/style-panel/style-panel.tsx +349 -0
  69. package/src/app/components/style-panel/use-design.ts +112 -0
  70. package/src/app/components/theme-toggle.tsx +59 -0
  71. package/src/app/components/themes/theme-detail.tsx +305 -0
  72. package/src/app/components/themes/themes-gallery.tsx +149 -0
  73. package/src/app/components/thumbnail-rail.tsx +805 -0
  74. package/src/app/components/ui/badge.tsx +45 -0
  75. package/src/app/components/ui/button.tsx +99 -0
  76. package/src/app/components/ui/card.tsx +92 -0
  77. package/src/app/components/ui/context-menu.tsx +237 -0
  78. package/src/app/components/ui/dialog.tsx +157 -0
  79. package/src/app/components/ui/dropdown-menu.tsx +245 -0
  80. package/src/app/components/ui/input.tsx +25 -0
  81. package/src/app/components/ui/label.tsx +24 -0
  82. package/src/app/components/ui/popover.tsx +75 -0
  83. package/src/app/components/ui/progress.tsx +31 -0
  84. package/src/app/components/ui/scroll-area.tsx +53 -0
  85. package/src/app/components/ui/select.tsx +196 -0
  86. package/src/app/components/ui/separator.tsx +28 -0
  87. package/src/app/components/ui/slider.tsx +61 -0
  88. package/src/app/components/ui/sonner.tsx +48 -0
  89. package/src/app/components/ui/tabs.tsx +79 -0
  90. package/src/app/components/ui/textarea.tsx +22 -0
  91. package/src/app/components/ui/toggle-group.tsx +83 -0
  92. package/src/app/components/ui/toggle.tsx +45 -0
  93. package/src/app/components/ui/tooltip.tsx +58 -0
  94. package/src/app/favicon.ico +0 -0
  95. package/src/app/index.html +13 -0
  96. package/src/app/lib/assets.ts +242 -0
  97. package/src/app/lib/design-presets.ts +94 -0
  98. package/src/app/lib/design.ts +58 -0
  99. package/src/app/lib/export-html.ts +326 -0
  100. package/src/app/lib/export-pdf.ts +298 -0
  101. package/src/app/lib/export-pptx.ts +284 -0
  102. package/src/app/lib/folders.ts +239 -0
  103. package/src/app/lib/inspector/fiber.test.ts +154 -0
  104. package/src/app/lib/inspector/fiber.ts +85 -0
  105. package/src/app/lib/inspector/use-comments.ts +74 -0
  106. package/src/app/lib/inspector/use-editor.ts +73 -0
  107. package/src/app/lib/inspector/use-notes.ts +134 -0
  108. package/src/app/lib/locale-store.ts +67 -0
  109. package/src/app/lib/page-context.tsx +38 -0
  110. package/src/app/lib/print-ready.test.ts +32 -0
  111. package/src/app/lib/print-ready.ts +51 -0
  112. package/src/app/lib/sdk.test.ts +13 -0
  113. package/src/app/lib/sdk.ts +37 -0
  114. package/src/app/lib/slides.ts +26 -0
  115. package/src/app/lib/step-context.tsx +261 -0
  116. package/src/app/lib/themes.ts +22 -0
  117. package/src/app/lib/transition.ts +30 -0
  118. package/src/app/lib/use-agent-socket.ts +18 -0
  119. package/src/app/lib/use-click-page-navigation.ts +60 -0
  120. package/src/app/lib/use-is-mobile.ts +21 -0
  121. package/src/app/lib/use-locale.ts +8 -0
  122. package/src/app/lib/use-prefers-reduced-motion.ts +19 -0
  123. package/src/app/lib/use-slide-module.ts +48 -0
  124. package/src/app/lib/use-wheel-page-navigation.ts +99 -0
  125. package/src/app/lib/utils.test.ts +25 -0
  126. package/src/app/lib/utils.ts +6 -0
  127. package/src/app/main.tsx +14 -0
  128. package/src/app/routes/assets.tsx +9 -0
  129. package/src/app/routes/home-shell.tsx +213 -0
  130. package/src/app/routes/home.tsx +807 -0
  131. package/src/app/routes/presenter.tsx +418 -0
  132. package/src/app/routes/slide.tsx +1108 -0
  133. package/src/app/routes/themes.tsx +34 -0
  134. package/src/app/styles.css +429 -0
  135. package/src/app/virtual.d.ts +51 -0
  136. package/src/locale/en.ts +416 -0
  137. package/src/locale/format.ts +12 -0
  138. package/src/locale/index.ts +6 -0
  139. package/src/locale/ja.ts +422 -0
  140. package/src/locale/types.ts +443 -0
  141. package/src/locale/zh-cn.ts +414 -0
  142. package/src/locale/zh-tw.ts +414 -0
@@ -0,0 +1,61 @@
1
+ import * as React from "react"
2
+ import { Slider as SliderPrimitive } from "radix-ui"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ function Slider({
7
+ className,
8
+ defaultValue,
9
+ value,
10
+ min = 0,
11
+ max = 100,
12
+ ...props
13
+ }: React.ComponentProps<typeof SliderPrimitive.Root>) {
14
+ const _values = React.useMemo(
15
+ () =>
16
+ Array.isArray(value)
17
+ ? value
18
+ : Array.isArray(defaultValue)
19
+ ? defaultValue
20
+ : [min, max],
21
+ [value, defaultValue, min, max]
22
+ )
23
+
24
+ return (
25
+ <SliderPrimitive.Root
26
+ data-slot="slider"
27
+ defaultValue={defaultValue}
28
+ value={value}
29
+ min={min}
30
+ max={max}
31
+ className={cn(
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
+ className
34
+ )}
35
+ {...props}
36
+ >
37
+ <SliderPrimitive.Track
38
+ data-slot="slider-track"
39
+ className={cn(
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
+ )}
42
+ >
43
+ <SliderPrimitive.Range
44
+ data-slot="slider-range"
45
+ className={cn(
46
+ "absolute bg-foreground data-[orientation=horizontal]:h-full data-[orientation=vertical]:w-full"
47
+ )}
48
+ />
49
+ </SliderPrimitive.Track>
50
+ {Array.from({ length: _values.length }, (_, index) => (
51
+ <SliderPrimitive.Thumb
52
+ data-slot="slider-thumb"
53
+ key={index}
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
+ />
56
+ ))}
57
+ </SliderPrimitive.Root>
58
+ )
59
+ }
60
+
61
+ export { Slider }
@@ -0,0 +1,48 @@
1
+ import {
2
+ CircleCheckIcon,
3
+ InfoIcon,
4
+ Loader2Icon,
5
+ OctagonXIcon,
6
+ TriangleAlertIcon,
7
+ } from "lucide-react"
8
+ import { useTheme } from "next-themes"
9
+ import { Toaster as Sonner, type ToasterProps } from "sonner"
10
+
11
+ const Toaster = ({ ...props }: ToasterProps) => {
12
+ const { theme = "system" } = useTheme()
13
+
14
+ return (
15
+ <Sonner
16
+ theme={theme as ToasterProps["theme"]}
17
+ className="toaster group"
18
+ icons={{
19
+ success: <CircleCheckIcon className="size-4" />,
20
+ info: <InfoIcon className="size-4" />,
21
+ warning: <TriangleAlertIcon className="size-4" />,
22
+ error: <OctagonXIcon className="size-4" />,
23
+ loading: <Loader2Icon className="size-4 animate-spin" />,
24
+ }}
25
+ style={
26
+ {
27
+ "--normal-bg": "var(--popover)",
28
+ "--normal-text": "var(--popover-foreground)",
29
+ "--normal-border": "var(--border)",
30
+ "--border-radius": "8px",
31
+ "--font-family":
32
+ "Geist Variable, -apple-system, BlinkMacSystemFont, sans-serif",
33
+ } as React.CSSProperties
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
+ }}
43
+ {...props}
44
+ />
45
+ )
46
+ }
47
+
48
+ export { Toaster }
@@ -0,0 +1,79 @@
1
+ import * as React from 'react';
2
+ import { cva, type VariantProps } from 'class-variance-authority';
3
+ import { Tabs as TabsPrimitive } from 'radix-ui';
4
+
5
+ import { cn } from '@/lib/utils';
6
+
7
+ function Tabs({
8
+ className,
9
+ orientation = 'horizontal',
10
+ ...props
11
+ }: React.ComponentProps<typeof TabsPrimitive.Root>) {
12
+ return (
13
+ <TabsPrimitive.Root
14
+ data-slot="tabs"
15
+ data-orientation={orientation}
16
+ orientation={orientation}
17
+ className={cn('group/tabs flex gap-2 data-[orientation=horizontal]:flex-col', className)}
18
+ {...props}
19
+ />
20
+ );
21
+ }
22
+
23
+ const tabsListVariants = cva(
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
+ {
26
+ variants: {
27
+ variant: {
28
+ default: 'bg-muted/70 ring-1 ring-inset ring-border/60',
29
+ line: 'gap-1 bg-transparent',
30
+ },
31
+ },
32
+ defaultVariants: {
33
+ variant: 'default',
34
+ },
35
+ },
36
+ );
37
+
38
+ function TabsList({
39
+ className,
40
+ variant = 'default',
41
+ ...props
42
+ }: React.ComponentProps<typeof TabsPrimitive.List> & VariantProps<typeof tabsListVariants>) {
43
+ return (
44
+ <TabsPrimitive.List
45
+ data-slot="tabs-list"
46
+ data-variant={variant}
47
+ className={cn(tabsListVariants({ variant }), className)}
48
+ {...props}
49
+ />
50
+ );
51
+ }
52
+
53
+ function TabsTrigger({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Trigger>) {
54
+ return (
55
+ <TabsPrimitive.Trigger
56
+ data-slot="tabs-trigger"
57
+ className={cn(
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
+ className,
63
+ )}
64
+ {...props}
65
+ />
66
+ );
67
+ }
68
+
69
+ function TabsContent({ className, ...props }: React.ComponentProps<typeof TabsPrimitive.Content>) {
70
+ return (
71
+ <TabsPrimitive.Content
72
+ data-slot="tabs-content"
73
+ className={cn('flex-1 outline-none', className)}
74
+ {...props}
75
+ />
76
+ );
77
+ }
78
+
79
+ export { Tabs, TabsList, TabsTrigger, TabsContent, tabsListVariants };
@@ -0,0 +1,22 @@
1
+ import * as React from 'react';
2
+
3
+ import { cn } from '@/lib/utils';
4
+
5
+ function Textarea({ className, ...props }: React.ComponentProps<'textarea'>) {
6
+ return (
7
+ <textarea
8
+ data-slot="textarea"
9
+ className={cn(
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,
16
+ )}
17
+ {...props}
18
+ />
19
+ );
20
+ }
21
+
22
+ export { Textarea };
@@ -0,0 +1,83 @@
1
+ "use client"
2
+
3
+ import * as React from "react"
4
+ import { type VariantProps } from "class-variance-authority"
5
+ import { ToggleGroup as ToggleGroupPrimitive } from "radix-ui"
6
+
7
+ import { cn } from "@/lib/utils"
8
+ import { toggleVariants } from "@/components/ui/toggle"
9
+
10
+ const ToggleGroupContext = React.createContext<
11
+ VariantProps<typeof toggleVariants> & {
12
+ spacing?: number
13
+ }
14
+ >({
15
+ size: "default",
16
+ variant: "default",
17
+ spacing: 0,
18
+ })
19
+
20
+ function ToggleGroup({
21
+ className,
22
+ variant,
23
+ size,
24
+ spacing = 0,
25
+ children,
26
+ ...props
27
+ }: React.ComponentProps<typeof ToggleGroupPrimitive.Root> &
28
+ VariantProps<typeof toggleVariants> & {
29
+ spacing?: number
30
+ }) {
31
+ return (
32
+ <ToggleGroupPrimitive.Root
33
+ data-slot="toggle-group"
34
+ data-variant={variant}
35
+ data-size={size}
36
+ data-spacing={spacing}
37
+ style={{ "--gap": spacing } as React.CSSProperties}
38
+ className={cn(
39
+ "group/toggle-group flex w-fit items-center gap-[--spacing(var(--gap))] rounded-[5px]",
40
+ className
41
+ )}
42
+ {...props}
43
+ >
44
+ <ToggleGroupContext.Provider value={{ variant, size, spacing }}>
45
+ {children}
46
+ </ToggleGroupContext.Provider>
47
+ </ToggleGroupPrimitive.Root>
48
+ )
49
+ }
50
+
51
+ function ToggleGroupItem({
52
+ className,
53
+ children,
54
+ variant,
55
+ size,
56
+ ...props
57
+ }: React.ComponentProps<typeof ToggleGroupPrimitive.Item> &
58
+ VariantProps<typeof toggleVariants>) {
59
+ const context = React.useContext(ToggleGroupContext)
60
+
61
+ return (
62
+ <ToggleGroupPrimitive.Item
63
+ data-slot="toggle-group-item"
64
+ data-variant={context.variant || variant}
65
+ data-size={context.size || size}
66
+ data-spacing={context.spacing}
67
+ className={cn(
68
+ toggleVariants({
69
+ variant: context.variant || variant,
70
+ size: context.size || size,
71
+ }),
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-[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
+ className
75
+ )}
76
+ {...props}
77
+ >
78
+ {children}
79
+ </ToggleGroupPrimitive.Item>
80
+ )
81
+ }
82
+
83
+ export { ToggleGroup, ToggleGroupItem }
@@ -0,0 +1,45 @@
1
+ import * as React from "react"
2
+ import { cva, type VariantProps } from "class-variance-authority"
3
+ import { Toggle as TogglePrimitive } from "radix-ui"
4
+
5
+ import { cn } from "@/lib/utils"
6
+
7
+ const toggleVariants = cva(
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
+ {
10
+ variants: {
11
+ variant: {
12
+ default: "bg-transparent text-foreground/70",
13
+ outline:
14
+ "border border-border bg-card text-foreground/75 hover:border-foreground/20 data-[state=on]:border-foreground",
15
+ },
16
+ size: {
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
+ },
21
+ },
22
+ defaultVariants: {
23
+ variant: "default",
24
+ size: "default",
25
+ },
26
+ }
27
+ )
28
+
29
+ function Toggle({
30
+ className,
31
+ variant,
32
+ size,
33
+ ...props
34
+ }: React.ComponentProps<typeof TogglePrimitive.Root> &
35
+ VariantProps<typeof toggleVariants>) {
36
+ return (
37
+ <TogglePrimitive.Root
38
+ data-slot="toggle"
39
+ className={cn(toggleVariants({ variant, size, className }))}
40
+ {...props}
41
+ />
42
+ )
43
+ }
44
+
45
+ export { Toggle, toggleVariants }
@@ -0,0 +1,58 @@
1
+ import * as React from "react"
2
+ import { Tooltip as TooltipPrimitive } from "radix-ui"
3
+
4
+ import { cn } from "@/lib/utils"
5
+
6
+ function TooltipProvider({
7
+ delayDuration = 0,
8
+ ...props
9
+ }: React.ComponentProps<typeof TooltipPrimitive.Provider>) {
10
+ return (
11
+ <TooltipPrimitive.Provider
12
+ data-slot="tooltip-provider"
13
+ delayDuration={delayDuration}
14
+ {...props}
15
+ />
16
+ )
17
+ }
18
+
19
+ function Tooltip({
20
+ ...props
21
+ }: React.ComponentProps<typeof TooltipPrimitive.Root>) {
22
+ return <TooltipPrimitive.Root data-slot="tooltip" {...props} />
23
+ }
24
+
25
+ function TooltipTrigger({
26
+ ...props
27
+ }: React.ComponentProps<typeof TooltipPrimitive.Trigger>) {
28
+ return <TooltipPrimitive.Trigger data-slot="tooltip-trigger" {...props} />
29
+ }
30
+
31
+ function TooltipContent({
32
+ className,
33
+ sideOffset = 0,
34
+ children,
35
+ container,
36
+ ...props
37
+ }: React.ComponentProps<typeof TooltipPrimitive.Content> & {
38
+ container?: React.ComponentProps<typeof TooltipPrimitive.Portal>['container']
39
+ }) {
40
+ return (
41
+ <TooltipPrimitive.Portal container={container}>
42
+ <TooltipPrimitive.Content
43
+ data-slot="tooltip-content"
44
+ sideOffset={sideOffset}
45
+ className={cn(
46
+ "z-50 w-fit origin-(--radix-tooltip-content-transform-origin) animate-in rounded-md bg-foreground px-3 py-1.5 text-xs text-balance text-background fade-in-0 zoom-in-95 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",
47
+ className
48
+ )}
49
+ {...props}
50
+ >
51
+ {children}
52
+ <TooltipPrimitive.Arrow className="z-50 size-2.5 translate-y-[calc(-50%_-_2px)] rotate-45 rounded-[2px] bg-foreground fill-foreground" />
53
+ </TooltipPrimitive.Content>
54
+ </TooltipPrimitive.Portal>
55
+ )
56
+ }
57
+
58
+ export { Tooltip, TooltipTrigger, TooltipContent, TooltipProvider }
Binary file
@@ -0,0 +1,13 @@
1
+ <!doctype html>
2
+ <html lang="en">
3
+ <head>
4
+ <meta charset="UTF-8" />
5
+ <link rel="icon" href="./favicon.ico" />
6
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
7
+ <title>open-aippt</title>
8
+ </head>
9
+ <body>
10
+ <div id="root"></div>
11
+ <script type="module" src="./main.tsx"></script>
12
+ </body>
13
+ </html>
@@ -0,0 +1,242 @@
1
+ import { useCallback, useEffect, useState } from 'react';
2
+
3
+ export type AssetEntry = {
4
+ name: string;
5
+ size: number;
6
+ mtime: number;
7
+ mime: string;
8
+ url: string;
9
+ unused: boolean;
10
+ };
11
+
12
+ export type UploadOptions = { overwrite?: boolean };
13
+
14
+ export async function listAssets(slideId: string): Promise<AssetEntry[]> {
15
+ const res = await fetch(`/__assets/${slideId}`);
16
+ if (!res.ok) throw new Error(`GET /__assets/${slideId} ${res.status}`);
17
+ const data = (await res.json()) as { assets?: AssetEntry[] };
18
+ return data.assets ?? [];
19
+ }
20
+
21
+ export async function uploadAsset(
22
+ slideId: string,
23
+ file: File,
24
+ opts: UploadOptions = {},
25
+ ): Promise<Response> {
26
+ const qs = opts.overwrite ? '?overwrite=1' : '';
27
+ return fetch(`/__assets/${slideId}/${encodeURIComponent(file.name)}${qs}`, {
28
+ method: 'POST',
29
+ headers: {
30
+ 'content-type': file.type || 'application/octet-stream',
31
+ 'content-length': String(file.size),
32
+ },
33
+ body: file,
34
+ });
35
+ }
36
+
37
+ async function renameAsset(slideId: string, from: string, to: string): Promise<Response> {
38
+ return fetch(`/__assets/${slideId}/${encodeURIComponent(from)}`, {
39
+ method: 'PATCH',
40
+ headers: { 'content-type': 'application/json' },
41
+ body: JSON.stringify({ name: to }),
42
+ });
43
+ }
44
+
45
+ async function deleteAsset(slideId: string, name: string): Promise<Response> {
46
+ return fetch(`/__assets/${slideId}/${encodeURIComponent(name)}`, { method: 'DELETE' });
47
+ }
48
+
49
+ export type AssetUsage = { slideId: string; count: number };
50
+
51
+ export async function listAssetUsages(slideId: string, name: string): Promise<AssetUsage[]> {
52
+ const res = await fetch(`/__assets/${slideId}/${encodeURIComponent(name)}/usages`);
53
+ if (!res.ok) return [];
54
+ const data = (await res.json().catch(() => null)) as { usages?: AssetUsage[] } | null;
55
+ return data?.usages ?? [];
56
+ }
57
+
58
+ export async function revertAssetUsage(
59
+ slideId: string,
60
+ assetPath: string,
61
+ ): Promise<{ ok: boolean; status: number }> {
62
+ const res = await fetch('/__edit/revert-asset', {
63
+ method: 'POST',
64
+ headers: { 'content-type': 'application/json' },
65
+ body: JSON.stringify({ slideId, assetPath }),
66
+ });
67
+ return { ok: res.ok, status: res.status };
68
+ }
69
+
70
+ export async function uploadWithAutoRename(
71
+ slideId: string,
72
+ file: File,
73
+ ): Promise<{ ok: boolean; status: number; entry: AssetEntry | null }> {
74
+ // Vite's default `assetsInclude` matches asset extensions case-sensitively,
75
+ // so `<img src="./assets/foo.JPG" />` (which the placeholder edit rewrites
76
+ // into a real `import`) fails to parse. Lowercase the extension so the
77
+ // import path is always one Vite recognizes.
78
+ let uploaded = lowercaseExtension(file);
79
+ let res = await uploadAsset(slideId, uploaded);
80
+ if (res.status === 409) {
81
+ const list = await listAssets(slideId);
82
+ const taken = new Set(list.map((a) => a.name));
83
+ uploaded = renamedCopy(uploaded, taken);
84
+ res = await uploadAsset(slideId, uploaded);
85
+ }
86
+ if (!res.ok) return { ok: false, status: res.status, entry: null };
87
+ const body = (await res.json().catch(() => null)) as Partial<AssetEntry> | null;
88
+ const entry: AssetEntry = {
89
+ name: body?.name ?? uploaded.name,
90
+ size: body?.size ?? uploaded.size,
91
+ mtime: body?.mtime ?? Date.now(),
92
+ mime: body?.mime ?? uploaded.type ?? 'application/octet-stream',
93
+ url: body?.url ?? `/__assets/${slideId}/${encodeURIComponent(uploaded.name)}`,
94
+ unused: body?.unused ?? false,
95
+ };
96
+ return { ok: true, status: res.status, entry };
97
+ }
98
+
99
+ function lowercaseExtension(file: File): File {
100
+ const dot = file.name.lastIndexOf('.');
101
+ if (dot <= 0) return file;
102
+ const ext = file.name.slice(dot);
103
+ const lower = ext.toLowerCase();
104
+ if (ext === lower) return file;
105
+ return new File([file], file.name.slice(0, dot) + lower, {
106
+ type: file.type,
107
+ lastModified: file.lastModified,
108
+ });
109
+ }
110
+
111
+ export function renamedCopy(file: File, taken: Set<string>): File {
112
+ const dot = file.name.lastIndexOf('.');
113
+ const stem = dot > 0 ? file.name.slice(0, dot) : file.name;
114
+ const ext = dot > 0 ? file.name.slice(dot) : '';
115
+ let i = 1;
116
+ let next = `${stem}-${i}${ext}`;
117
+ while (taken.has(next)) {
118
+ i += 1;
119
+ next = `${stem}-${i}${ext}`;
120
+ }
121
+ return new File([file], next, { type: file.type, lastModified: file.lastModified });
122
+ }
123
+
124
+ export type SvglItem = {
125
+ id: number;
126
+ title: string;
127
+ category: string | string[];
128
+ route: string | { light: string; dark: string };
129
+ url: string;
130
+ };
131
+
132
+ export async function searchSvgl(query: string, signal?: AbortSignal): Promise<SvglItem[]> {
133
+ const q = query.trim();
134
+ const params = new URLSearchParams();
135
+ if (q) params.set('q', q);
136
+ else params.set('limit', '24');
137
+ const res = await fetch(`/__svgl/search?${params.toString()}`, { signal });
138
+ // svgl returns 404 when a search has no matches — treat it as an empty list,
139
+ // not an error.
140
+ if (res.status === 404) return [];
141
+ if (!res.ok) throw new Error(`svgl ${res.status}`);
142
+ return (await res.json()) as SvglItem[];
143
+ }
144
+
145
+ export function svgProxyUrl(routeUrl: string): string {
146
+ return `/__svgl/svg?u=${encodeURIComponent(routeUrl)}`;
147
+ }
148
+
149
+ export async function fetchSvgAsFile(routeUrl: string, filename: string): Promise<File> {
150
+ const res = await fetch(svgProxyUrl(routeUrl));
151
+ if (!res.ok) throw new Error(`svgl route ${res.status}`);
152
+ const blob = await res.blob();
153
+ return new File([blob], filename, { type: 'image/svg+xml' });
154
+ }
155
+
156
+ export type UseAssetsResult = {
157
+ assets: AssetEntry[];
158
+ loading: boolean;
159
+ available: boolean;
160
+ upload: (file: File, opts?: UploadOptions) => Promise<{ ok: boolean; status: number }>;
161
+ rename: (from: string, to: string) => Promise<{ ok: boolean; status: number }>;
162
+ remove: (name: string) => Promise<{ ok: boolean; status: number }>;
163
+ refresh: () => Promise<void>;
164
+ };
165
+
166
+ const NOOP_RESULT = { ok: false, status: 0 } as const;
167
+
168
+ export function useAssets(slideId: string): UseAssetsResult {
169
+ const available = import.meta.env.DEV;
170
+ const [assets, setAssets] = useState<AssetEntry[]>([]);
171
+ const [loading, setLoading] = useState(available);
172
+
173
+ const refresh = useCallback(async () => {
174
+ if (!available) return;
175
+ const next = await listAssets(slideId);
176
+ setAssets(next);
177
+ }, [slideId]);
178
+
179
+ useEffect(() => {
180
+ if (!available) return;
181
+ let cancelled = false;
182
+ setLoading(true);
183
+ listAssets(slideId)
184
+ .then((next) => {
185
+ if (!cancelled) {
186
+ setAssets(next);
187
+ setLoading(false);
188
+ }
189
+ })
190
+ .catch(() => {
191
+ if (!cancelled) setLoading(false);
192
+ });
193
+ return () => {
194
+ cancelled = true;
195
+ };
196
+ }, [slideId]);
197
+
198
+ useEffect(() => {
199
+ if (!available || !import.meta.hot) return;
200
+ const handler = (data: { slideId?: string } | undefined) => {
201
+ if (!data || data.slideId === slideId) {
202
+ refresh().catch(() => {});
203
+ }
204
+ };
205
+ import.meta.hot.on('open-aippt:assets-changed', handler);
206
+ return () => {
207
+ import.meta.hot?.off('open-aippt:assets-changed', handler);
208
+ };
209
+ }, [slideId, refresh]);
210
+
211
+ const upload = useCallback(
212
+ async (file: File, opts?: UploadOptions) => {
213
+ if (!available) return NOOP_RESULT;
214
+ const res = await uploadAsset(slideId, file, opts);
215
+ if (res.ok) await refresh();
216
+ return { ok: res.ok, status: res.status };
217
+ },
218
+ [slideId, refresh],
219
+ );
220
+
221
+ const rename = useCallback(
222
+ async (from: string, to: string) => {
223
+ if (!available) return NOOP_RESULT;
224
+ const res = await renameAsset(slideId, from, to);
225
+ if (res.ok) await refresh();
226
+ return { ok: res.ok, status: res.status };
227
+ },
228
+ [slideId, refresh],
229
+ );
230
+
231
+ const remove = useCallback(
232
+ async (name: string) => {
233
+ if (!available) return NOOP_RESULT;
234
+ const res = await deleteAsset(slideId, name);
235
+ if (res.ok) await refresh();
236
+ return { ok: res.ok, status: res.status };
237
+ },
238
+ [slideId, refresh],
239
+ );
240
+
241
+ return { assets, loading, available, upload, rename, remove, refresh };
242
+ }