@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,66 @@
|
|
|
1
|
+
import { type RefObject, useEffect, useRef } from 'react';
|
|
2
|
+
|
|
3
|
+
const MIN_SWIPE_PX = 50;
|
|
4
|
+
const MAX_SWIPE_MS = 600;
|
|
5
|
+
|
|
6
|
+
type Options<T extends HTMLElement> = {
|
|
7
|
+
ref: RefObject<T | null>;
|
|
8
|
+
enabled?: boolean;
|
|
9
|
+
onPrev: () => void;
|
|
10
|
+
onNext: () => void;
|
|
11
|
+
};
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Single-finger horizontal swipe → prev/next. Vertical-dominant gestures
|
|
15
|
+
* are left alone so scroll-y on tablets keeps working. The handler only
|
|
16
|
+
* binds when `enabled`, so overlay layers can suppress it.
|
|
17
|
+
*/
|
|
18
|
+
export function useTouchSwipe<T extends HTMLElement>({
|
|
19
|
+
ref,
|
|
20
|
+
enabled = true,
|
|
21
|
+
onPrev,
|
|
22
|
+
onNext,
|
|
23
|
+
}: Options<T>) {
|
|
24
|
+
const start = useRef<{ x: number; y: number; t: number } | null>(null);
|
|
25
|
+
|
|
26
|
+
useEffect(() => {
|
|
27
|
+
const el = ref.current;
|
|
28
|
+
if (!el || !enabled) return;
|
|
29
|
+
|
|
30
|
+
const onStart = (e: TouchEvent) => {
|
|
31
|
+
if (e.touches.length !== 1) {
|
|
32
|
+
start.current = null;
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
const t = e.touches[0];
|
|
36
|
+
start.current = { x: t.clientX, y: t.clientY, t: performance.now() };
|
|
37
|
+
};
|
|
38
|
+
const onEnd = (e: TouchEvent) => {
|
|
39
|
+
const s = start.current;
|
|
40
|
+
start.current = null;
|
|
41
|
+
if (!s) return;
|
|
42
|
+
const t = e.changedTouches[0];
|
|
43
|
+
if (!t) return;
|
|
44
|
+
const dx = t.clientX - s.x;
|
|
45
|
+
const dy = t.clientY - s.y;
|
|
46
|
+
if (performance.now() - s.t > MAX_SWIPE_MS) return;
|
|
47
|
+
if (Math.abs(dx) < MIN_SWIPE_PX) return;
|
|
48
|
+
if (Math.abs(dx) <= Math.abs(dy)) return;
|
|
49
|
+
if (dx < 0) onNext();
|
|
50
|
+
else onPrev();
|
|
51
|
+
};
|
|
52
|
+
|
|
53
|
+
const onCancel = () => {
|
|
54
|
+
start.current = null;
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
el.addEventListener('touchstart', onStart, { passive: true });
|
|
58
|
+
el.addEventListener('touchend', onEnd);
|
|
59
|
+
el.addEventListener('touchcancel', onCancel);
|
|
60
|
+
return () => {
|
|
61
|
+
el.removeEventListener('touchstart', onStart);
|
|
62
|
+
el.removeEventListener('touchend', onEnd);
|
|
63
|
+
el.removeEventListener('touchcancel', onCancel);
|
|
64
|
+
};
|
|
65
|
+
}, [ref, enabled, onPrev, onNext]);
|
|
66
|
+
}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import {
|
|
2
|
+
Children,
|
|
3
|
+
type CSSProperties,
|
|
4
|
+
cloneElement,
|
|
5
|
+
isValidElement,
|
|
6
|
+
type ReactElement,
|
|
7
|
+
type ReactNode,
|
|
8
|
+
} from 'react';
|
|
9
|
+
|
|
10
|
+
export type unstable_SharedElementProps = {
|
|
11
|
+
id: string;
|
|
12
|
+
children: ReactNode;
|
|
13
|
+
className?: string;
|
|
14
|
+
style?: CSSProperties;
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
type SharedElementChildProps = {
|
|
18
|
+
className?: string;
|
|
19
|
+
style?: CSSProperties;
|
|
20
|
+
'data-osd-shared-element'?: string;
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function unstable_SharedElement({
|
|
24
|
+
id,
|
|
25
|
+
children,
|
|
26
|
+
className,
|
|
27
|
+
style,
|
|
28
|
+
}: unstable_SharedElementProps) {
|
|
29
|
+
const child = Children.toArray(children)[0] ?? null;
|
|
30
|
+
|
|
31
|
+
if (
|
|
32
|
+
Children.count(children) === 1 &&
|
|
33
|
+
isValidElement<SharedElementChildProps>(child) &&
|
|
34
|
+
typeof child.type === 'string'
|
|
35
|
+
) {
|
|
36
|
+
return cloneElement(child as ReactElement<SharedElementChildProps>, {
|
|
37
|
+
'data-osd-shared-element': id,
|
|
38
|
+
className: [child.props.className, className].filter(Boolean).join(' ') || undefined,
|
|
39
|
+
style: style ? { ...child.props.style, ...style } : child.props.style,
|
|
40
|
+
});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
return (
|
|
44
|
+
<div className={className} style={style} data-osd-shared-element={id}>
|
|
45
|
+
{children}
|
|
46
|
+
</div>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
@@ -0,0 +1,258 @@
|
|
|
1
|
+
import { MoreHorizontal, Pencil, Trash2 } from 'lucide-react';
|
|
2
|
+
import { useEffect, useRef, useState } from 'react';
|
|
3
|
+
import {
|
|
4
|
+
DropdownMenu,
|
|
5
|
+
DropdownMenuContent,
|
|
6
|
+
DropdownMenuItem,
|
|
7
|
+
DropdownMenuTrigger,
|
|
8
|
+
} from '@/components/ui/dropdown-menu';
|
|
9
|
+
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
10
|
+
import type { Folder, FolderIcon } from '@/lib/sdk';
|
|
11
|
+
import { useLocale } from '@/lib/use-locale';
|
|
12
|
+
import { cn } from '@/lib/utils';
|
|
13
|
+
import { IconPicker } from './icon-picker';
|
|
14
|
+
|
|
15
|
+
export const SLIDE_DND_MIME = 'application/x-slide-id';
|
|
16
|
+
|
|
17
|
+
function useSlideDragActive() {
|
|
18
|
+
const [active, setActive] = useState(false);
|
|
19
|
+
useEffect(() => {
|
|
20
|
+
const onStart = (e: DragEvent) => {
|
|
21
|
+
if (e.dataTransfer?.types?.includes(SLIDE_DND_MIME)) setActive(true);
|
|
22
|
+
};
|
|
23
|
+
const onEnd = () => setActive(false);
|
|
24
|
+
document.addEventListener('dragstart', onStart);
|
|
25
|
+
document.addEventListener('dragend', onEnd);
|
|
26
|
+
document.addEventListener('drop', onEnd);
|
|
27
|
+
return () => {
|
|
28
|
+
document.removeEventListener('dragstart', onStart);
|
|
29
|
+
document.removeEventListener('dragend', onEnd);
|
|
30
|
+
document.removeEventListener('drop', onEnd);
|
|
31
|
+
};
|
|
32
|
+
}, []);
|
|
33
|
+
return active;
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
export function FolderIconChip({ icon, className }: { icon: FolderIcon; className?: string }) {
|
|
37
|
+
if (icon.type === 'emoji') {
|
|
38
|
+
return (
|
|
39
|
+
<span
|
|
40
|
+
className={cn(
|
|
41
|
+
'inline-flex size-5 items-center justify-center text-[15px] leading-none',
|
|
42
|
+
className,
|
|
43
|
+
)}
|
|
44
|
+
>
|
|
45
|
+
{icon.value}
|
|
46
|
+
</span>
|
|
47
|
+
);
|
|
48
|
+
}
|
|
49
|
+
return (
|
|
50
|
+
<span
|
|
51
|
+
className={cn(
|
|
52
|
+
'inline-block size-3 rounded-[3px] ring-1 ring-foreground/15 shadow-[inset_0_1px_0_oklch(1_0_0/0.18)]',
|
|
53
|
+
className,
|
|
54
|
+
)}
|
|
55
|
+
style={{ background: icon.value }}
|
|
56
|
+
/>
|
|
57
|
+
);
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
type Row =
|
|
61
|
+
| {
|
|
62
|
+
kind: 'folder';
|
|
63
|
+
folder: Folder;
|
|
64
|
+
onRename: (name: string) => void;
|
|
65
|
+
onChangeIcon: (icon: FolderIcon) => void;
|
|
66
|
+
onDelete: () => void;
|
|
67
|
+
}
|
|
68
|
+
| {
|
|
69
|
+
kind: 'draft';
|
|
70
|
+
}
|
|
71
|
+
| {
|
|
72
|
+
kind: 'themes';
|
|
73
|
+
}
|
|
74
|
+
| {
|
|
75
|
+
kind: 'assets';
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
export function FolderItem({
|
|
79
|
+
row,
|
|
80
|
+
count,
|
|
81
|
+
selected,
|
|
82
|
+
onSelect,
|
|
83
|
+
onDropSlide,
|
|
84
|
+
}: {
|
|
85
|
+
row: Row;
|
|
86
|
+
count: number;
|
|
87
|
+
selected: boolean;
|
|
88
|
+
onSelect: () => void;
|
|
89
|
+
onDropSlide: (slideId: string) => void;
|
|
90
|
+
}) {
|
|
91
|
+
const [renaming, setRenaming] = useState(false);
|
|
92
|
+
const [dragOver, setDragOver] = useState(false);
|
|
93
|
+
const dragDepth = useRef(0);
|
|
94
|
+
const [draftName, setDraftName] = useState(row.kind === 'folder' ? row.folder.name : '');
|
|
95
|
+
const slideDragActive = useSlideDragActive();
|
|
96
|
+
const t = useLocale();
|
|
97
|
+
|
|
98
|
+
const acceptsSlideDrop = row.kind !== 'themes' && row.kind !== 'assets';
|
|
99
|
+
const isSlideDrag = (e: React.DragEvent) =>
|
|
100
|
+
acceptsSlideDrop && e.dataTransfer.types.includes(SLIDE_DND_MIME);
|
|
101
|
+
const handleDragEnter = (e: React.DragEvent) => {
|
|
102
|
+
if (!isSlideDrag(e)) return;
|
|
103
|
+
dragDepth.current += 1;
|
|
104
|
+
if (dragDepth.current === 1) setDragOver(true);
|
|
105
|
+
};
|
|
106
|
+
const handleDragOver = (e: React.DragEvent) => {
|
|
107
|
+
if (!isSlideDrag(e)) return;
|
|
108
|
+
e.preventDefault();
|
|
109
|
+
e.dataTransfer.dropEffect = 'move';
|
|
110
|
+
};
|
|
111
|
+
const handleDragLeave = (e: React.DragEvent) => {
|
|
112
|
+
if (!isSlideDrag(e)) return;
|
|
113
|
+
dragDepth.current = Math.max(0, dragDepth.current - 1);
|
|
114
|
+
if (dragDepth.current === 0) setDragOver(false);
|
|
115
|
+
};
|
|
116
|
+
const handleDrop = (e: React.DragEvent) => {
|
|
117
|
+
if (!acceptsSlideDrop) return;
|
|
118
|
+
const slideId = e.dataTransfer.getData(SLIDE_DND_MIME);
|
|
119
|
+
dragDepth.current = 0;
|
|
120
|
+
setDragOver(false);
|
|
121
|
+
if (!slideId) return;
|
|
122
|
+
e.preventDefault();
|
|
123
|
+
onDropSlide(slideId);
|
|
124
|
+
};
|
|
125
|
+
|
|
126
|
+
const icon: FolderIcon =
|
|
127
|
+
row.kind === 'draft'
|
|
128
|
+
? { type: 'emoji', value: '📝' }
|
|
129
|
+
: row.kind === 'themes'
|
|
130
|
+
? { type: 'emoji', value: '🎨' }
|
|
131
|
+
: row.kind === 'assets'
|
|
132
|
+
? { type: 'emoji', value: '🗂️' }
|
|
133
|
+
: row.folder.icon;
|
|
134
|
+
const label =
|
|
135
|
+
row.kind === 'draft'
|
|
136
|
+
? t.home.draft
|
|
137
|
+
: row.kind === 'themes'
|
|
138
|
+
? t.home.themes
|
|
139
|
+
: row.kind === 'assets'
|
|
140
|
+
? t.home.assets
|
|
141
|
+
: row.folder.name;
|
|
142
|
+
|
|
143
|
+
const commitRename = () => {
|
|
144
|
+
if (row.kind !== 'folder') return;
|
|
145
|
+
const trimmed = draftName.trim();
|
|
146
|
+
if (trimmed && trimmed !== row.folder.name) row.onRename(trimmed);
|
|
147
|
+
setRenaming(false);
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
return (
|
|
151
|
+
// biome-ignore lint/a11y/noStaticElementInteractions: drag-and-drop target wraps interactive children
|
|
152
|
+
<div
|
|
153
|
+
className={cn(
|
|
154
|
+
'group relative flex items-center gap-2.5 rounded-[5px] px-2 py-[5px] text-[12.5px] transition-colors',
|
|
155
|
+
selected
|
|
156
|
+
? 'bg-muted text-foreground before:absolute before:inset-y-1.5 before:-left-0.5 before:w-[2px] before:rounded-full before:bg-brand'
|
|
157
|
+
: 'text-foreground/70 hover:bg-muted/60 hover:text-foreground',
|
|
158
|
+
slideDragActive && acceptsSlideDrop && !dragOver && 'ring-1 ring-foreground/10',
|
|
159
|
+
dragOver &&
|
|
160
|
+
'bg-brand/10 text-foreground ring-1 ring-brand ring-offset-1 ring-offset-sidebar motion-safe:scale-[1.01] motion-safe:transition-transform',
|
|
161
|
+
)}
|
|
162
|
+
onDragEnter={handleDragEnter}
|
|
163
|
+
onDragOver={handleDragOver}
|
|
164
|
+
onDragLeave={handleDragLeave}
|
|
165
|
+
onDrop={handleDrop}
|
|
166
|
+
>
|
|
167
|
+
{row.kind === 'folder' && import.meta.env.DEV ? (
|
|
168
|
+
<Popover>
|
|
169
|
+
<PopoverTrigger asChild>
|
|
170
|
+
<button
|
|
171
|
+
type="button"
|
|
172
|
+
className="flex size-5 shrink-0 items-center justify-center rounded transition-transform hover:scale-110"
|
|
173
|
+
aria-label={t.home.changeIcon}
|
|
174
|
+
onClick={(e) => e.stopPropagation()}
|
|
175
|
+
>
|
|
176
|
+
<FolderIconChip icon={icon} />
|
|
177
|
+
</button>
|
|
178
|
+
</PopoverTrigger>
|
|
179
|
+
<PopoverContent side="right" align="start" className="w-auto p-2">
|
|
180
|
+
<IconPicker value={row.folder.icon} onChange={(next) => row.onChangeIcon(next)} />
|
|
181
|
+
</PopoverContent>
|
|
182
|
+
</Popover>
|
|
183
|
+
) : (
|
|
184
|
+
<button
|
|
185
|
+
type="button"
|
|
186
|
+
onClick={onSelect}
|
|
187
|
+
aria-label={label}
|
|
188
|
+
className="flex size-5 shrink-0 items-center justify-center"
|
|
189
|
+
>
|
|
190
|
+
<FolderIconChip icon={icon} />
|
|
191
|
+
</button>
|
|
192
|
+
)}
|
|
193
|
+
|
|
194
|
+
{renaming && row.kind === 'folder' ? (
|
|
195
|
+
<input
|
|
196
|
+
value={draftName}
|
|
197
|
+
onChange={(e) => setDraftName(e.target.value)}
|
|
198
|
+
onBlur={commitRename}
|
|
199
|
+
onKeyDown={(e) => {
|
|
200
|
+
if (e.nativeEvent.isComposing) return;
|
|
201
|
+
if (e.key === 'Enter') commitRename();
|
|
202
|
+
if (e.key === 'Escape') {
|
|
203
|
+
setDraftName(row.folder.name);
|
|
204
|
+
setRenaming(false);
|
|
205
|
+
}
|
|
206
|
+
}}
|
|
207
|
+
maxLength={40}
|
|
208
|
+
className="min-w-0 flex-1 rounded-[3px] bg-card px-1 text-[12.5px] outline-none ring-1 ring-foreground/20"
|
|
209
|
+
/>
|
|
210
|
+
) : (
|
|
211
|
+
<button type="button" onClick={onSelect} className="min-w-0 flex-1 truncate text-left">
|
|
212
|
+
{label}
|
|
213
|
+
</button>
|
|
214
|
+
)}
|
|
215
|
+
|
|
216
|
+
<span
|
|
217
|
+
className={cn(
|
|
218
|
+
'folio ml-auto shrink-0 transition-opacity',
|
|
219
|
+
row.kind === 'folder' &&
|
|
220
|
+
import.meta.env.DEV &&
|
|
221
|
+
'group-hover:opacity-0 group-has-[[aria-expanded=true]]:opacity-0',
|
|
222
|
+
)}
|
|
223
|
+
>
|
|
224
|
+
{count.toString().padStart(2, '0')}
|
|
225
|
+
</span>
|
|
226
|
+
|
|
227
|
+
{row.kind === 'folder' && import.meta.env.DEV && (
|
|
228
|
+
<DropdownMenu>
|
|
229
|
+
<DropdownMenuTrigger asChild>
|
|
230
|
+
<button
|
|
231
|
+
type="button"
|
|
232
|
+
onClick={(e) => e.stopPropagation()}
|
|
233
|
+
className="absolute right-2 top-1/2 size-5 -translate-y-1/2 rounded opacity-0 transition-opacity hover:bg-foreground/10 group-hover:opacity-100 aria-expanded:opacity-100"
|
|
234
|
+
aria-label={t.home.folderActions}
|
|
235
|
+
>
|
|
236
|
+
<MoreHorizontal className="mx-auto size-3.5" />
|
|
237
|
+
</button>
|
|
238
|
+
</DropdownMenuTrigger>
|
|
239
|
+
<DropdownMenuContent align="end" className="min-w-[140px]">
|
|
240
|
+
<DropdownMenuItem
|
|
241
|
+
onSelect={() => {
|
|
242
|
+
setDraftName(row.folder.name);
|
|
243
|
+
setRenaming(true);
|
|
244
|
+
}}
|
|
245
|
+
>
|
|
246
|
+
<Pencil />
|
|
247
|
+
{t.common.rename}
|
|
248
|
+
</DropdownMenuItem>
|
|
249
|
+
<DropdownMenuItem variant="destructive" onSelect={() => row.onDelete()}>
|
|
250
|
+
<Trash2 />
|
|
251
|
+
{t.common.delete}
|
|
252
|
+
</DropdownMenuItem>
|
|
253
|
+
</DropdownMenuContent>
|
|
254
|
+
</DropdownMenu>
|
|
255
|
+
)}
|
|
256
|
+
</div>
|
|
257
|
+
);
|
|
258
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import EmojiPicker, { EmojiStyle, Theme } from 'emoji-picker-react';
|
|
2
|
+
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
3
|
+
import type { FolderIcon } from '@/lib/sdk';
|
|
4
|
+
import { useLocale } from '@/lib/use-locale';
|
|
5
|
+
|
|
6
|
+
export const PRESET_COLORS = [
|
|
7
|
+
'#c0392b', // vermillion
|
|
8
|
+
'#b8743e', // ochre
|
|
9
|
+
'#6f7a3a', // olive
|
|
10
|
+
'#2f6a4f', // forest
|
|
11
|
+
'#3a5a7c', // ink blue
|
|
12
|
+
'#6b4675', // plum
|
|
13
|
+
'#a3543b', // terracotta
|
|
14
|
+
'#3a3a3a', // graphite
|
|
15
|
+
];
|
|
16
|
+
|
|
17
|
+
export function IconPicker({
|
|
18
|
+
value,
|
|
19
|
+
onChange,
|
|
20
|
+
}: {
|
|
21
|
+
value: FolderIcon;
|
|
22
|
+
onChange: (icon: FolderIcon) => void;
|
|
23
|
+
}) {
|
|
24
|
+
const t = useLocale();
|
|
25
|
+
return (
|
|
26
|
+
<Tabs defaultValue={value.type} className="w-[320px]">
|
|
27
|
+
<TabsList className="w-full">
|
|
28
|
+
<TabsTrigger value="emoji">{t.home.iconEmojiTab}</TabsTrigger>
|
|
29
|
+
<TabsTrigger value="color">{t.home.iconColorTab}</TabsTrigger>
|
|
30
|
+
</TabsList>
|
|
31
|
+
|
|
32
|
+
<TabsContent value="emoji">
|
|
33
|
+
<EmojiPicker
|
|
34
|
+
lazyLoadEmojis
|
|
35
|
+
emojiStyle={EmojiStyle.NATIVE}
|
|
36
|
+
theme={Theme.AUTO}
|
|
37
|
+
width="100%"
|
|
38
|
+
height={360}
|
|
39
|
+
onEmojiClick={(data) => onChange({ type: 'emoji', value: data.emoji })}
|
|
40
|
+
previewConfig={{ showPreview: false }}
|
|
41
|
+
skinTonesDisabled
|
|
42
|
+
/>
|
|
43
|
+
</TabsContent>
|
|
44
|
+
|
|
45
|
+
<TabsContent value="color">
|
|
46
|
+
<div className="grid grid-cols-8 gap-1.5 py-2">
|
|
47
|
+
{PRESET_COLORS.map((c) => (
|
|
48
|
+
<button
|
|
49
|
+
key={c}
|
|
50
|
+
type="button"
|
|
51
|
+
onClick={() => onChange({ type: 'color', value: c })}
|
|
52
|
+
className="size-6 rounded-[4px] ring-1 ring-foreground/10 shadow-[inset_0_1px_0_oklch(1_0_0/0.18)] transition-transform hover:scale-110"
|
|
53
|
+
style={{ background: c }}
|
|
54
|
+
aria-label={c}
|
|
55
|
+
/>
|
|
56
|
+
))}
|
|
57
|
+
</div>
|
|
58
|
+
</TabsContent>
|
|
59
|
+
</Tabs>
|
|
60
|
+
);
|
|
61
|
+
}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import { cn } from '@/lib/utils';
|
|
2
|
+
import type { FolderIcon } from '../../lib/sdk';
|
|
3
|
+
import { FolderIconChip } from './folder-item';
|
|
4
|
+
|
|
5
|
+
export function MobileFolderPill({
|
|
6
|
+
icon,
|
|
7
|
+
label,
|
|
8
|
+
count,
|
|
9
|
+
active,
|
|
10
|
+
onClick,
|
|
11
|
+
}: {
|
|
12
|
+
icon: FolderIcon;
|
|
13
|
+
label: string;
|
|
14
|
+
count: number;
|
|
15
|
+
active: boolean;
|
|
16
|
+
onClick: () => void;
|
|
17
|
+
}) {
|
|
18
|
+
return (
|
|
19
|
+
<button
|
|
20
|
+
type="button"
|
|
21
|
+
onClick={onClick}
|
|
22
|
+
className={cn(
|
|
23
|
+
'flex shrink-0 items-center gap-1.5 rounded-[5px] border px-2.5 py-1 text-[11.5px] font-medium transition-colors',
|
|
24
|
+
active
|
|
25
|
+
? 'border-foreground/40 bg-foreground text-background'
|
|
26
|
+
: 'border-border bg-card text-muted-foreground hover:text-foreground',
|
|
27
|
+
)}
|
|
28
|
+
>
|
|
29
|
+
<FolderIconChip icon={icon} className="size-3.5 text-sm" />
|
|
30
|
+
<span className="max-w-[8rem] truncate">{label}</span>
|
|
31
|
+
<span className="folio nums">{count.toString().padStart(2, '0')}</span>
|
|
32
|
+
</button>
|
|
33
|
+
);
|
|
34
|
+
}
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import config from 'virtual:open-aippt/config';
|
|
2
|
+
import { Loader2, RefreshCw } from 'lucide-react';
|
|
3
|
+
import { useEffect, useState } from 'react';
|
|
4
|
+
import { toast } from 'sonner';
|
|
5
|
+
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
7
|
+
import { format, useLocale } from '@/lib/use-locale';
|
|
8
|
+
|
|
9
|
+
type UpdateCheck = { current: string; latest: string | null; outdated: boolean };
|
|
10
|
+
type UpdateStatus = 'idle' | 'running' | 'done' | 'error';
|
|
11
|
+
|
|
12
|
+
export function SidebarFooter() {
|
|
13
|
+
const t = useLocale();
|
|
14
|
+
const [update, setUpdate] = useState<UpdateCheck | null>(null);
|
|
15
|
+
const [updateStatus, setUpdateStatus] = useState<UpdateStatus>('idle');
|
|
16
|
+
|
|
17
|
+
useEffect(() => {
|
|
18
|
+
if (!import.meta.env.DEV) return;
|
|
19
|
+
let cancelled = false;
|
|
20
|
+
fetch('/__update-check')
|
|
21
|
+
.then((res) => (res.ok ? (res.json() as Promise<UpdateCheck>) : null))
|
|
22
|
+
.then((data) => {
|
|
23
|
+
if (!cancelled && data?.outdated) setUpdate(data);
|
|
24
|
+
})
|
|
25
|
+
.catch(() => {});
|
|
26
|
+
return () => {
|
|
27
|
+
cancelled = true;
|
|
28
|
+
};
|
|
29
|
+
}, []);
|
|
30
|
+
|
|
31
|
+
const label = `v${config.version}`;
|
|
32
|
+
const isUpdating = updateStatus === 'running';
|
|
33
|
+
|
|
34
|
+
async function updatePackage() {
|
|
35
|
+
if (isUpdating) return;
|
|
36
|
+
setUpdateStatus('running');
|
|
37
|
+
try {
|
|
38
|
+
const res = await fetch('/__update-package', { method: 'POST' });
|
|
39
|
+
if (!res.ok) throw new Error('update failed');
|
|
40
|
+
setUpdateStatus('done');
|
|
41
|
+
toast.success(t.home.updatePackageDone);
|
|
42
|
+
} catch {
|
|
43
|
+
setUpdateStatus('error');
|
|
44
|
+
toast.error(t.home.updatePackageFailed);
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const versionRow = (
|
|
49
|
+
<span className="inline-flex cursor-default items-center gap-1.5">
|
|
50
|
+
{update?.latest && <span className="size-1.5 rounded-full bg-brand" aria-hidden />}
|
|
51
|
+
{label}
|
|
52
|
+
</span>
|
|
53
|
+
);
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<div className="px-4 py-3 text-[11px] text-muted-foreground/70 tabular-nums">
|
|
57
|
+
{update?.latest ? (
|
|
58
|
+
<TooltipProvider delayDuration={200}>
|
|
59
|
+
<Tooltip>
|
|
60
|
+
<TooltipTrigger asChild>{versionRow}</TooltipTrigger>
|
|
61
|
+
<TooltipContent
|
|
62
|
+
side="top"
|
|
63
|
+
align="start"
|
|
64
|
+
alignOffset={-8}
|
|
65
|
+
sideOffset={9}
|
|
66
|
+
collisionPadding={12}
|
|
67
|
+
className="flex w-[232px] max-w-[calc(100vw-24px)] flex-col gap-2.5 rounded-[8px] border border-background/10 bg-foreground/95 p-2.5 text-[11.5px] leading-4 shadow-[0_12px_32px_oklch(0_0_0/0.28)] backdrop-blur"
|
|
68
|
+
>
|
|
69
|
+
<span className="pr-1 text-background/92">
|
|
70
|
+
{format(t.home.updateAvailable, { version: update.latest })}
|
|
71
|
+
</span>
|
|
72
|
+
<Button
|
|
73
|
+
type="button"
|
|
74
|
+
size="xs"
|
|
75
|
+
variant="secondary"
|
|
76
|
+
className="h-6 w-fit rounded-[5px] border border-background/15 bg-background/8 px-2 text-[11px] text-background shadow-none hover:bg-background/14"
|
|
77
|
+
disabled={isUpdating || updateStatus === 'done'}
|
|
78
|
+
onClick={updatePackage}
|
|
79
|
+
>
|
|
80
|
+
{isUpdating ? (
|
|
81
|
+
<Loader2 className="animate-spin" aria-hidden />
|
|
82
|
+
) : (
|
|
83
|
+
<RefreshCw aria-hidden />
|
|
84
|
+
)}
|
|
85
|
+
{isUpdating ? t.home.updatingPackage : t.home.updatePackage}
|
|
86
|
+
</Button>
|
|
87
|
+
{updateStatus === 'done' && (
|
|
88
|
+
<span className="text-[11px] leading-4 text-background/65">
|
|
89
|
+
{t.home.updatePackageDone}
|
|
90
|
+
</span>
|
|
91
|
+
)}
|
|
92
|
+
{updateStatus === 'error' && (
|
|
93
|
+
<span className="text-[11px] leading-4 text-background/65">
|
|
94
|
+
{t.home.updatePackageFailed}
|
|
95
|
+
</span>
|
|
96
|
+
)}
|
|
97
|
+
</TooltipContent>
|
|
98
|
+
</Tooltip>
|
|
99
|
+
</TooltipProvider>
|
|
100
|
+
) : (
|
|
101
|
+
versionRow
|
|
102
|
+
)}
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|