@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.
- package/LICENSE +21 -0
- package/README.md +98 -0
- package/bin.js +2 -0
- package/dist/build-DxTqmvsO.js +17 -0
- package/dist/cli/bin.d.ts +1 -0
- package/dist/cli/bin.js +86 -0
- package/dist/config-CjzqjrEA.js +4280 -0
- package/dist/config-DIC-yVPp.d.ts +23 -0
- package/dist/design-cpzS8aud.js +35 -0
- package/dist/dev-BYuTeJbA.js +20 -0
- package/dist/format-BCeKbTOM.js +1605 -0
- package/dist/index.d.ts +134 -0
- package/dist/index.js +467 -0
- package/dist/locale/index.d.ts +24 -0
- package/dist/locale/index.js +3 -0
- package/dist/preview-DlQvnJPq.js +18 -0
- package/dist/sync-BPZ0m27m.js +139 -0
- package/dist/sync-EsYusbbL.js +3 -0
- package/dist/types-CHmFPIG_.d.ts +430 -0
- package/dist/vite/index.d.ts +14 -0
- package/dist/vite/index.js +4 -0
- package/env.d.ts +59 -0
- package/package.json +103 -0
- package/skills/apply-comments/SKILL.md +83 -0
- package/skills/create-slide/SKILL.md +91 -0
- package/skills/create-theme/SKILL.md +250 -0
- package/skills/current-slide/SKILL.md +110 -0
- package/skills/slide-authoring/SKILL.md +625 -0
- package/src/app/app.tsx +47 -0
- package/src/app/components/asset-view.tsx +966 -0
- package/src/app/components/history-provider.tsx +120 -0
- package/src/app/components/image-placeholder.tsx +243 -0
- package/src/app/components/inspector/asset-picker-dialog.tsx +196 -0
- package/src/app/components/inspector/comment-widget.tsx +93 -0
- package/src/app/components/inspector/image-crop-dialog.tsx +212 -0
- package/src/app/components/inspector/inspect-overlay.tsx +387 -0
- package/src/app/components/inspector/inspector-panel.tsx +1115 -0
- package/src/app/components/inspector/inspector-provider.tsx +1218 -0
- package/src/app/components/inspector/save-bar.tsx +48 -0
- package/src/app/components/language-toggle.tsx +39 -0
- package/src/app/components/notes-drawer.tsx +120 -0
- package/src/app/components/overview-grid.tsx +363 -0
- package/src/app/components/panel/panel-fields.tsx +60 -0
- package/src/app/components/panel/panel-shell.tsx +80 -0
- package/src/app/components/panel/save-card.tsx +142 -0
- package/src/app/components/pdf-progress-toast.tsx +32 -0
- package/src/app/components/player.tsx +466 -0
- package/src/app/components/pptx-progress-toast.tsx +32 -0
- package/src/app/components/present/blackout-overlay.tsx +18 -0
- package/src/app/components/present/control-bar.tsx +315 -0
- package/src/app/components/present/help-overlay.tsx +57 -0
- package/src/app/components/present/jump-input.tsx +74 -0
- package/src/app/components/present/laser-pointer.tsx +39 -0
- package/src/app/components/present/progress-bar.tsx +26 -0
- package/src/app/components/present/use-idle.ts +46 -0
- package/src/app/components/present/use-pointer-near-bottom.ts +34 -0
- package/src/app/components/present/use-presenter-channel.ts +66 -0
- package/src/app/components/present/use-touch-swipe.ts +66 -0
- package/src/app/components/shared-element.tsx +48 -0
- package/src/app/components/sidebar/folder-item.tsx +258 -0
- package/src/app/components/sidebar/icon-picker.tsx +61 -0
- package/src/app/components/sidebar/mobile-pill.tsx +34 -0
- package/src/app/components/sidebar/sidebar-footer.tsx +105 -0
- package/src/app/components/sidebar/sidebar.tsx +284 -0
- package/src/app/components/slide-canvas.tsx +102 -0
- package/src/app/components/slide-transition-layer.tsx +844 -0
- package/src/app/components/style-panel/design-provider.tsx +148 -0
- package/src/app/components/style-panel/style-panel.tsx +349 -0
- package/src/app/components/style-panel/use-design.ts +112 -0
- package/src/app/components/theme-toggle.tsx +59 -0
- package/src/app/components/themes/theme-detail.tsx +305 -0
- package/src/app/components/themes/themes-gallery.tsx +149 -0
- package/src/app/components/thumbnail-rail.tsx +805 -0
- package/src/app/components/ui/badge.tsx +45 -0
- package/src/app/components/ui/button.tsx +99 -0
- package/src/app/components/ui/card.tsx +92 -0
- package/src/app/components/ui/context-menu.tsx +237 -0
- package/src/app/components/ui/dialog.tsx +157 -0
- package/src/app/components/ui/dropdown-menu.tsx +245 -0
- package/src/app/components/ui/input.tsx +25 -0
- package/src/app/components/ui/label.tsx +24 -0
- package/src/app/components/ui/popover.tsx +75 -0
- package/src/app/components/ui/progress.tsx +31 -0
- package/src/app/components/ui/scroll-area.tsx +53 -0
- package/src/app/components/ui/select.tsx +196 -0
- package/src/app/components/ui/separator.tsx +28 -0
- package/src/app/components/ui/slider.tsx +61 -0
- package/src/app/components/ui/sonner.tsx +48 -0
- package/src/app/components/ui/tabs.tsx +79 -0
- package/src/app/components/ui/textarea.tsx +22 -0
- package/src/app/components/ui/toggle-group.tsx +83 -0
- package/src/app/components/ui/toggle.tsx +45 -0
- package/src/app/components/ui/tooltip.tsx +58 -0
- package/src/app/favicon.ico +0 -0
- package/src/app/index.html +13 -0
- package/src/app/lib/assets.ts +242 -0
- package/src/app/lib/design-presets.ts +94 -0
- package/src/app/lib/design.ts +58 -0
- package/src/app/lib/export-html.ts +326 -0
- package/src/app/lib/export-pdf.ts +298 -0
- package/src/app/lib/export-pptx.ts +284 -0
- package/src/app/lib/folders.ts +239 -0
- package/src/app/lib/inspector/fiber.test.ts +154 -0
- package/src/app/lib/inspector/fiber.ts +85 -0
- package/src/app/lib/inspector/use-comments.ts +74 -0
- package/src/app/lib/inspector/use-editor.ts +73 -0
- package/src/app/lib/inspector/use-notes.ts +134 -0
- package/src/app/lib/locale-store.ts +67 -0
- package/src/app/lib/page-context.tsx +38 -0
- package/src/app/lib/print-ready.test.ts +32 -0
- package/src/app/lib/print-ready.ts +51 -0
- package/src/app/lib/sdk.test.ts +13 -0
- package/src/app/lib/sdk.ts +37 -0
- package/src/app/lib/slides.ts +26 -0
- package/src/app/lib/step-context.tsx +261 -0
- package/src/app/lib/themes.ts +22 -0
- package/src/app/lib/transition.ts +30 -0
- package/src/app/lib/use-agent-socket.ts +18 -0
- package/src/app/lib/use-click-page-navigation.ts +60 -0
- package/src/app/lib/use-is-mobile.ts +21 -0
- package/src/app/lib/use-locale.ts +8 -0
- package/src/app/lib/use-prefers-reduced-motion.ts +19 -0
- package/src/app/lib/use-slide-module.ts +48 -0
- package/src/app/lib/use-wheel-page-navigation.ts +99 -0
- package/src/app/lib/utils.test.ts +25 -0
- package/src/app/lib/utils.ts +6 -0
- package/src/app/main.tsx +14 -0
- package/src/app/routes/assets.tsx +9 -0
- package/src/app/routes/home-shell.tsx +213 -0
- package/src/app/routes/home.tsx +807 -0
- package/src/app/routes/presenter.tsx +418 -0
- package/src/app/routes/slide.tsx +1108 -0
- package/src/app/routes/themes.tsx +34 -0
- package/src/app/styles.css +429 -0
- package/src/app/virtual.d.ts +51 -0
- package/src/locale/en.ts +416 -0
- package/src/locale/format.ts +12 -0
- package/src/locale/index.ts +6 -0
- package/src/locale/ja.ts +422 -0
- package/src/locale/types.ts +443 -0
- package/src/locale/zh-cn.ts +414 -0
- 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
|
+
}
|