@lemonriver/core 0.1.1 → 0.2.0
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/dist/{build-f-fM2ZY4.js → build-Df_xwIpg.js} +1 -1
- package/dist/cli/bin.js +4 -4
- package/dist/{config-DO45GU3e.js → config-BjWCLoMQ.js} +2343 -363
- package/dist/{config-B6JaKJM4.d.ts → config-rMGCkz6i.d.ts} +6 -2
- package/dist/{dev-CqS2xpgE.js → dev-Ctsx9IeO.js} +2 -2
- package/dist/index.d.ts +3 -3
- package/dist/index.js +4 -3
- package/dist/locale/index.d.ts +5 -2
- package/dist/locale/index.js +2 -2
- package/dist/{preview-BihgMlEl.js → preview-CJgcqsPk.js} +1 -1
- package/dist/{types-BjW4glip.d.ts → types-8VUyA2bu.d.ts} +133 -5
- package/dist/{update-C_R0ycVC.js → update-BKOA3Z6o.js} +153 -21
- package/dist/vite/index.d.ts +1 -1
- package/dist/vite/index.js +1 -1
- package/dist/{zh-tw-Ci0jK8eg.js → zh-tw-CeLrYwRE.js} +1178 -25
- package/package.json +13 -6
- package/skills/apply-comments/SKILL.md +10 -6
- package/skills/create-slide/SKILL.md +2 -0
- package/skills/current-slide/SKILL.md +1 -0
- package/skills/slide-authoring/SKILL.md +31 -1
- package/src/agent/prompt.ts +141 -0
- package/src/agent/provider.ts +123 -0
- package/src/agent/types.ts +151 -0
- package/src/app/app.tsx +2 -0
- package/src/app/assets/open-slide.svg +39 -0
- package/src/app/components/agent/agent-dock.tsx +35 -0
- package/src/app/components/agent/agent-markdown.tsx +45 -0
- package/src/app/components/agent/agent-panel.tsx +692 -0
- package/src/app/components/agent/agent-presence.tsx +74 -0
- package/src/app/components/agent/composer.tsx +120 -0
- package/src/app/components/agent/connect-view.tsx +184 -0
- package/src/app/components/agent/history-view.tsx +135 -0
- package/src/app/components/agent/provider-logo.tsx +34 -0
- package/src/app/components/agent/transcript.tsx +609 -0
- package/src/app/components/board/board-canvas.tsx +143 -12
- package/src/app/components/board/board.css +51 -0
- package/src/app/components/board/layout.test.ts +53 -1
- package/src/app/components/board/layout.ts +50 -2
- package/src/app/components/board/page-node.tsx +287 -13
- package/src/app/components/board/use-board-cards.ts +210 -0
- package/src/app/components/board/use-page-order.ts +17 -9
- package/src/app/components/command/slide-command-menu.tsx +10 -0
- package/src/app/components/icon-switcher-indicator.tsx +1 -1
- package/src/app/components/inspector/comment-layer.tsx +1011 -0
- package/src/app/components/inspector/image-crop-dialog.tsx +1 -5
- package/src/app/components/inspector/inspect-overlay.tsx +2 -39
- package/src/app/components/inspector/inspector-panel.tsx +10 -93
- package/src/app/components/inspector/inspector-provider.tsx +121 -47
- package/src/app/components/panel/editor-sidebar.tsx +85 -16
- package/src/app/components/panel/panel-shell.tsx +2 -2
- package/src/app/components/panel/resize-handle.tsx +129 -0
- package/src/app/components/player.tsx +0 -1
- package/src/app/components/preview-step-host.tsx +16 -0
- package/src/app/components/sidebar/sidebar.tsx +2 -2
- package/src/app/components/style-panel/style-panel.tsx +3 -2
- package/src/app/components/thumbnail-rail.tsx +76 -1
- package/src/app/favicon.ico +0 -0
- package/src/app/favicon.svg +39 -0
- package/src/app/index.html +2 -1
- package/src/app/lib/agent/use-agent.ts +543 -0
- package/src/app/lib/inspector/freeze-motion.ts +61 -0
- package/src/app/lib/inspector/use-comments.ts +32 -3
- package/src/app/lib/locale-store.ts +6 -1
- package/src/app/lib/print-ready.ts +9 -6
- package/src/app/lib/use-auto-grow.ts +15 -0
- package/src/app/lib/utils.ts +4 -0
- package/src/app/routes/board.tsx +48 -11
- package/src/app/routes/home.tsx +43 -7
- package/src/app/routes/presenter.tsx +1 -16
- package/src/app/routes/preview.tsx +169 -0
- package/src/app/routes/slide.tsx +221 -173
- package/src/app/styles.css +234 -80
- package/src/editing/placeholder-id.ts +1 -0
- package/src/locale/en.ts +127 -5
- package/src/locale/es.ts +692 -0
- package/src/locale/index.ts +1 -0
- package/src/locale/ja.ts +120 -6
- package/src/locale/types.ts +132 -5
- package/src/locale/zh-cn.ts +117 -5
- package/src/locale/zh-tw.ts +117 -5
- package/src/app/assets/open-slide.png +0 -0
- package/src/app/components/inspector/comment-widget.tsx +0 -119
|
@@ -0,0 +1,129 @@
|
|
|
1
|
+
import { useEffect, useRef, useState } from 'react';
|
|
2
|
+
import { clamp, cn } from '@/lib/utils';
|
|
3
|
+
|
|
4
|
+
export function useStoredWidth(key: string, fallback: number, min: number, max: number) {
|
|
5
|
+
const [width, setWidth] = useState<number>(() => {
|
|
6
|
+
try {
|
|
7
|
+
const parsed = Number(window.localStorage.getItem(key) ?? Number.NaN);
|
|
8
|
+
return Number.isFinite(parsed) ? clamp(parsed, min, max) : fallback;
|
|
9
|
+
} catch {
|
|
10
|
+
return fallback;
|
|
11
|
+
}
|
|
12
|
+
});
|
|
13
|
+
useEffect(() => {
|
|
14
|
+
try {
|
|
15
|
+
window.localStorage.setItem(key, String(width));
|
|
16
|
+
} catch {}
|
|
17
|
+
}, [key, width]);
|
|
18
|
+
return [width, setWidth] as const;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
// A vertical splitter on one edge of a panel. `edge` is the side of the panel
|
|
22
|
+
// it sits on, so dragging away from the panel always makes it wider.
|
|
23
|
+
export function ResizeHandle({
|
|
24
|
+
width,
|
|
25
|
+
min,
|
|
26
|
+
max,
|
|
27
|
+
defaultWidth,
|
|
28
|
+
edge,
|
|
29
|
+
label,
|
|
30
|
+
onResize,
|
|
31
|
+
onResizingChange,
|
|
32
|
+
className,
|
|
33
|
+
}: {
|
|
34
|
+
width: number;
|
|
35
|
+
min: number;
|
|
36
|
+
max: number;
|
|
37
|
+
defaultWidth: number;
|
|
38
|
+
edge: 'left' | 'right';
|
|
39
|
+
label: string;
|
|
40
|
+
onResize: (width: number) => void;
|
|
41
|
+
onResizingChange?: (resizing: boolean) => void;
|
|
42
|
+
className?: string;
|
|
43
|
+
}) {
|
|
44
|
+
const [resizing, setResizing] = useState(false);
|
|
45
|
+
const dragRef = useRef<{ startX: number; startWidth: number } | null>(null);
|
|
46
|
+
const direction = edge === 'right' ? 1 : -1;
|
|
47
|
+
|
|
48
|
+
useEffect(() => {
|
|
49
|
+
onResizingChange?.(resizing);
|
|
50
|
+
if (!resizing) return;
|
|
51
|
+
const prev = {
|
|
52
|
+
cursor: document.body.style.cursor,
|
|
53
|
+
userSelect: document.body.style.userSelect,
|
|
54
|
+
};
|
|
55
|
+
document.body.style.cursor = 'col-resize';
|
|
56
|
+
document.body.style.userSelect = 'none';
|
|
57
|
+
return () => {
|
|
58
|
+
document.body.style.cursor = prev.cursor;
|
|
59
|
+
document.body.style.userSelect = prev.userSelect;
|
|
60
|
+
};
|
|
61
|
+
}, [resizing, onResizingChange]);
|
|
62
|
+
|
|
63
|
+
const onPointerDown = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
64
|
+
e.preventDefault();
|
|
65
|
+
e.currentTarget.setPointerCapture(e.pointerId);
|
|
66
|
+
dragRef.current = { startX: e.clientX, startWidth: width };
|
|
67
|
+
setResizing(true);
|
|
68
|
+
};
|
|
69
|
+
|
|
70
|
+
const onPointerMove = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
71
|
+
if (!dragRef.current) return;
|
|
72
|
+
const delta = (e.clientX - dragRef.current.startX) * direction;
|
|
73
|
+
onResize(clamp(dragRef.current.startWidth + delta, min, max));
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
const onPointerUp = (e: React.PointerEvent<HTMLDivElement>) => {
|
|
77
|
+
if (e.currentTarget.hasPointerCapture(e.pointerId)) {
|
|
78
|
+
e.currentTarget.releasePointerCapture(e.pointerId);
|
|
79
|
+
}
|
|
80
|
+
dragRef.current = null;
|
|
81
|
+
setResizing(false);
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
const onKeyDown = (e: React.KeyboardEvent<HTMLDivElement>) => {
|
|
85
|
+
const step = (e.shiftKey ? 32 : 8) * direction;
|
|
86
|
+
let next: number | null = null;
|
|
87
|
+
if (e.key === 'ArrowLeft') next = width - step;
|
|
88
|
+
else if (e.key === 'ArrowRight') next = width + step;
|
|
89
|
+
else if (e.key === 'Home') next = defaultWidth;
|
|
90
|
+
if (next === null) return;
|
|
91
|
+
e.preventDefault();
|
|
92
|
+
e.stopPropagation();
|
|
93
|
+
onResize(clamp(next, min, max));
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
return (
|
|
97
|
+
// biome-ignore lint/a11y/useSemanticElements: focusable resize handle (splitter pattern), not a static <hr>
|
|
98
|
+
<div
|
|
99
|
+
role="separator"
|
|
100
|
+
aria-orientation="vertical"
|
|
101
|
+
aria-label={label}
|
|
102
|
+
aria-valuenow={width}
|
|
103
|
+
aria-valuemin={min}
|
|
104
|
+
aria-valuemax={max}
|
|
105
|
+
tabIndex={0}
|
|
106
|
+
onPointerDown={onPointerDown}
|
|
107
|
+
onPointerMove={onPointerMove}
|
|
108
|
+
onPointerUp={onPointerUp}
|
|
109
|
+
onPointerCancel={onPointerUp}
|
|
110
|
+
onKeyDown={onKeyDown}
|
|
111
|
+
onDoubleClick={() => onResize(defaultWidth)}
|
|
112
|
+
className={cn(
|
|
113
|
+
'group/resize absolute inset-y-0 z-20 w-1.5 cursor-col-resize touch-none outline-none',
|
|
114
|
+
'focus-visible:bg-brand/20',
|
|
115
|
+
className,
|
|
116
|
+
)}
|
|
117
|
+
>
|
|
118
|
+
<span
|
|
119
|
+
aria-hidden
|
|
120
|
+
className={cn(
|
|
121
|
+
'pointer-events-none absolute inset-y-0 w-px bg-brand opacity-0 transition-opacity duration-150',
|
|
122
|
+
edge === 'right' ? 'left-1/2 -translate-x-1/2' : 'left-0',
|
|
123
|
+
'group-hover/resize:opacity-100 group-focus-visible/resize:opacity-100',
|
|
124
|
+
resizing && 'opacity-100',
|
|
125
|
+
)}
|
|
126
|
+
/>
|
|
127
|
+
</div>
|
|
128
|
+
);
|
|
129
|
+
}
|
|
@@ -402,7 +402,6 @@ export function Player({
|
|
|
402
402
|
controls && 'select-none',
|
|
403
403
|
controls && (hideCursor ? 'cursor-none' : 'cursor-default'),
|
|
404
404
|
)}
|
|
405
|
-
style={design ? { background: design.palette.bg } : undefined}
|
|
406
405
|
>
|
|
407
406
|
<SlideCanvas flat design={design}>
|
|
408
407
|
{/* Keyed per deck so a presenter-driven deck switch cuts instead of
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
import { type ReactNode, useRef } from 'react';
|
|
2
|
+
import { type StepController, StepHost } from '../lib/step-context';
|
|
3
|
+
|
|
4
|
+
export function PreviewStepHost({ revealed, children }: { revealed: number; children: ReactNode }) {
|
|
5
|
+
const noopControllerRef = useRef<StepController | null>(null);
|
|
6
|
+
return (
|
|
7
|
+
<StepHost
|
|
8
|
+
isActivePage={false}
|
|
9
|
+
entryDirection="jump"
|
|
10
|
+
controllerRef={noopControllerRef}
|
|
11
|
+
controlledRevealed={revealed}
|
|
12
|
+
>
|
|
13
|
+
{children}
|
|
14
|
+
</StepHost>
|
|
15
|
+
);
|
|
16
|
+
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { Plus, Search } from 'lucide-react';
|
|
2
2
|
import { useEffect, useRef, useState } from 'react';
|
|
3
3
|
import { toast } from 'sonner';
|
|
4
|
-
import logo from '@/assets/open-slide.
|
|
4
|
+
import logo from '@/assets/open-slide.svg';
|
|
5
5
|
import { Popover, PopoverContent, PopoverTrigger } from '@/components/ui/popover';
|
|
6
6
|
import type { Folder, FolderIcon } from '@/lib/sdk';
|
|
7
7
|
import { format, useLocale } from '@/lib/use-locale';
|
|
@@ -135,7 +135,7 @@ export function Sidebar({
|
|
|
135
135
|
alt=""
|
|
136
136
|
aria-hidden
|
|
137
137
|
draggable={false}
|
|
138
|
-
className="size-6 shrink-0 select-none
|
|
138
|
+
className="size-6 shrink-0 select-none"
|
|
139
139
|
/>
|
|
140
140
|
<h1 className="font-heading text-[13.5px] font-semibold tracking-tight">
|
|
141
141
|
{t.home.appTitle}
|
|
@@ -197,12 +197,13 @@ export function DesignToggleButton({
|
|
|
197
197
|
size="sm"
|
|
198
198
|
variant={active ? 'default' : 'ghost'}
|
|
199
199
|
onClick={onToggle}
|
|
200
|
+
aria-label={t.stylePanel.designToggle}
|
|
200
201
|
data-design-ui
|
|
201
202
|
title={t.stylePanel.designToggleTitle}
|
|
202
203
|
>
|
|
203
204
|
<Palette className="size-3.5" />
|
|
204
|
-
<span className="hidden
|
|
205
|
-
<kbd className="ml-1 hidden rounded-[3px] bg-foreground/10 px-1 font-mono text-[9.5px] tracking-[0.04em]
|
|
205
|
+
<span className="hidden lg:inline">{t.stylePanel.designToggle}</span>
|
|
206
|
+
<kbd className="ml-1 hidden rounded-[3px] bg-foreground/10 px-1 font-mono text-[9.5px] tracking-[0.04em] lg:inline">
|
|
206
207
|
D
|
|
207
208
|
</kbd>
|
|
208
209
|
</Button>
|
|
@@ -27,6 +27,7 @@ import {
|
|
|
27
27
|
Workflow,
|
|
28
28
|
} from 'lucide-react';
|
|
29
29
|
import { Fragment, useCallback, useEffect, useLayoutEffect, useRef, useState } from 'react';
|
|
30
|
+
import { useOptionalInspector } from '@/components/inspector/inspector-provider';
|
|
30
31
|
import { Button } from '@/components/ui/button';
|
|
31
32
|
import {
|
|
32
33
|
ContextMenu,
|
|
@@ -37,7 +38,7 @@ import {
|
|
|
37
38
|
} from '@/components/ui/context-menu';
|
|
38
39
|
import { ScrollArea } from '@/components/ui/scroll-area';
|
|
39
40
|
import { Tooltip, TooltipContent, TooltipProvider, TooltipTrigger } from '@/components/ui/tooltip';
|
|
40
|
-
import { format, useLocale } from '@/lib/use-locale';
|
|
41
|
+
import { format, plural, useLocale } from '@/lib/use-locale';
|
|
41
42
|
import { cn, pad2 } from '@/lib/utils';
|
|
42
43
|
import type { DesignSystem } from '../lib/design';
|
|
43
44
|
import { SlidePageProvider } from '../lib/page-context';
|
|
@@ -77,6 +78,36 @@ type Props = {
|
|
|
77
78
|
onBoard?: () => void;
|
|
78
79
|
};
|
|
79
80
|
|
|
81
|
+
// Per-page pending comment counts, shown on the rail only while the
|
|
82
|
+
// comment tool is active.
|
|
83
|
+
function usePageCommentCounts(): Map<number, number> | null {
|
|
84
|
+
const inspector = useOptionalInspector();
|
|
85
|
+
if (!inspector?.commentMode) return null;
|
|
86
|
+
const counts = new Map<number, number>();
|
|
87
|
+
for (const comment of inspector.comments) {
|
|
88
|
+
if (comment.page === undefined) continue;
|
|
89
|
+
counts.set(comment.page, (counts.get(comment.page) ?? 0) + 1);
|
|
90
|
+
}
|
|
91
|
+
return counts;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function CommentCountBadge({ count, className }: { count: number; className?: string }) {
|
|
95
|
+
const t = useLocale();
|
|
96
|
+
return (
|
|
97
|
+
<span
|
|
98
|
+
role="img"
|
|
99
|
+
aria-label={format(plural(count, t.inspector.commentsCount), { count })}
|
|
100
|
+
data-page-comments={count}
|
|
101
|
+
className={cn(
|
|
102
|
+
'flex h-4 min-w-4 items-center justify-center rounded-full bg-brand px-1 font-mono text-[9.5px] font-semibold leading-none text-brand-foreground tabular-nums',
|
|
103
|
+
className,
|
|
104
|
+
)}
|
|
105
|
+
>
|
|
106
|
+
{count}
|
|
107
|
+
</span>
|
|
108
|
+
);
|
|
109
|
+
}
|
|
110
|
+
|
|
80
111
|
const DEFAULT_VERTICAL_THUMB_WIDTH = 184;
|
|
81
112
|
const VERTICAL_RAIL_CHROME = 80;
|
|
82
113
|
const MIN_VERTICAL_THUMB_WIDTH = 120;
|
|
@@ -107,6 +138,7 @@ export function ThumbnailRail({
|
|
|
107
138
|
const virtualListRef = useRef<HTMLDivElement | null>(null);
|
|
108
139
|
const verticalViewportRef = useRef<HTMLElement | null>(null);
|
|
109
140
|
const focusCurrentAfterScrollRef = useRef(false);
|
|
141
|
+
const focusAfterArrowRef = useRef(false);
|
|
110
142
|
const [currentPosition, setCurrentPosition] = useState<ThumbnailOffscreenDirection>(null);
|
|
111
143
|
const t = useLocale();
|
|
112
144
|
|
|
@@ -176,6 +208,32 @@ export function ThumbnailRail({
|
|
|
176
208
|
return () => cancelAnimationFrame(frame);
|
|
177
209
|
}, [currentPosition]);
|
|
178
210
|
|
|
211
|
+
useLayoutEffect(() => {
|
|
212
|
+
if (!focusAfterArrowRef.current || !activeRef.current) return;
|
|
213
|
+
activeRef.current.focus({ preventScroll: true });
|
|
214
|
+
focusAfterArrowRef.current = false;
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
const onThumbnailArrowKeyDown = useCallback(
|
|
218
|
+
(event: React.KeyboardEvent<HTMLButtonElement>, index: number) => {
|
|
219
|
+
if (event.altKey || event.ctrlKey || event.metaKey || event.nativeEvent.isComposing) return;
|
|
220
|
+
const direction =
|
|
221
|
+
event.key === 'ArrowRight' || event.key === 'ArrowDown'
|
|
222
|
+
? 1
|
|
223
|
+
: event.key === 'ArrowLeft' || event.key === 'ArrowUp'
|
|
224
|
+
? -1
|
|
225
|
+
: 0;
|
|
226
|
+
if (!direction) return;
|
|
227
|
+
event.preventDefault();
|
|
228
|
+
event.stopPropagation();
|
|
229
|
+
const next = Math.min(Math.max(index + direction, 0), pages.length - 1);
|
|
230
|
+
if (next === index) return;
|
|
231
|
+
focusAfterArrowRef.current = true;
|
|
232
|
+
onSelect(next);
|
|
233
|
+
},
|
|
234
|
+
[onSelect, pages.length],
|
|
235
|
+
);
|
|
236
|
+
|
|
179
237
|
const renderThumb = useCallback(
|
|
180
238
|
(PageComp: Page, i: number) => {
|
|
181
239
|
const active = i === current;
|
|
@@ -199,6 +257,7 @@ export function ThumbnailRail({
|
|
|
199
257
|
active={active}
|
|
200
258
|
activeRef={active ? activeRef : undefined}
|
|
201
259
|
onSelect={() => onSelect(i)}
|
|
260
|
+
onArrowKeyDown={(event) => onThumbnailArrowKeyDown(event, i)}
|
|
202
261
|
ariaLabel={format(t.thumbnailRail.goToPageAria, { n: i + 1 })}
|
|
203
262
|
>
|
|
204
263
|
{inner}
|
|
@@ -208,6 +267,7 @@ export function ThumbnailRail({
|
|
|
208
267
|
type="button"
|
|
209
268
|
ref={active ? activeRef : undefined}
|
|
210
269
|
onClick={() => onSelect(i)}
|
|
270
|
+
onKeyDown={(event) => onThumbnailArrowKeyDown(event, i)}
|
|
211
271
|
aria-label={format(t.thumbnailRail.goToPageAria, { n: i + 1 })}
|
|
212
272
|
aria-current={active ? 'page' : undefined}
|
|
213
273
|
className={thumbButtonClass(active)}
|
|
@@ -239,6 +299,7 @@ export function ThumbnailRail({
|
|
|
239
299
|
moduleTransition,
|
|
240
300
|
onReorder,
|
|
241
301
|
onSelect,
|
|
302
|
+
onThumbnailArrowKeyDown,
|
|
242
303
|
pages.length,
|
|
243
304
|
scale,
|
|
244
305
|
thumbWidth,
|
|
@@ -260,6 +321,7 @@ export function ThumbnailRail({
|
|
|
260
321
|
actions={actions}
|
|
261
322
|
activeRef={activeRef}
|
|
262
323
|
onSelect={onSelect}
|
|
324
|
+
onArrowKeyDown={onThumbnailArrowKeyDown}
|
|
263
325
|
scale={scale}
|
|
264
326
|
thumbWidth={horizontalWidth}
|
|
265
327
|
/>
|
|
@@ -438,6 +500,7 @@ function HorizontalVirtualThumbList({
|
|
|
438
500
|
actions,
|
|
439
501
|
activeRef,
|
|
440
502
|
onSelect,
|
|
503
|
+
onArrowKeyDown,
|
|
441
504
|
scale,
|
|
442
505
|
thumbWidth,
|
|
443
506
|
}: {
|
|
@@ -447,6 +510,7 @@ function HorizontalVirtualThumbList({
|
|
|
447
510
|
actions?: ThumbnailActions;
|
|
448
511
|
activeRef: React.MutableRefObject<HTMLButtonElement | null>;
|
|
449
512
|
onSelect: (index: number) => void;
|
|
513
|
+
onArrowKeyDown: (event: React.KeyboardEvent<HTMLButtonElement>, index: number) => void;
|
|
450
514
|
scale: number;
|
|
451
515
|
thumbWidth: number;
|
|
452
516
|
}) {
|
|
@@ -516,15 +580,18 @@ function HorizontalVirtualThumbList({
|
|
|
516
580
|
}, [current, itemWidth, pages.length, thumbWidth]);
|
|
517
581
|
|
|
518
582
|
const visibleRange = clampVisibleRange(range, current, pages.length);
|
|
583
|
+
const commentCounts = usePageCommentCounts();
|
|
519
584
|
const visible = [];
|
|
520
585
|
for (let i = visibleRange.start; i < visibleRange.end; i++) {
|
|
521
586
|
const PageComp = pages[i];
|
|
522
587
|
const active = i === current;
|
|
588
|
+
const commentCount = commentCounts?.get(i) ?? 0;
|
|
523
589
|
const button = (
|
|
524
590
|
<button
|
|
525
591
|
type="button"
|
|
526
592
|
ref={active ? activeRef : undefined}
|
|
527
593
|
onClick={() => onSelect(i)}
|
|
594
|
+
onKeyDown={(event) => onArrowKeyDown(event, i)}
|
|
528
595
|
aria-label={format(t.thumbnailRail.goToPageAria, { n: i + 1 })}
|
|
529
596
|
aria-current={active ? 'page' : undefined}
|
|
530
597
|
className={cn(
|
|
@@ -555,6 +622,9 @@ function HorizontalVirtualThumbList({
|
|
|
555
622
|
</SlidePageProvider>
|
|
556
623
|
</SlideCanvas>
|
|
557
624
|
</div>
|
|
625
|
+
{commentCount > 0 && (
|
|
626
|
+
<CommentCountBadge count={commentCount} className="absolute -top-0.5 -right-1" />
|
|
627
|
+
)}
|
|
558
628
|
</button>
|
|
559
629
|
);
|
|
560
630
|
|
|
@@ -791,6 +861,7 @@ function ThumbContents({
|
|
|
791
861
|
}, [PageComp]);
|
|
792
862
|
|
|
793
863
|
const hasTransition = Boolean(PageComp.transition ?? moduleTransition);
|
|
864
|
+
const commentCount = usePageCommentCounts()?.get(index) ?? 0;
|
|
794
865
|
|
|
795
866
|
return (
|
|
796
867
|
<>
|
|
@@ -803,6 +874,7 @@ function ThumbContents({
|
|
|
803
874
|
>
|
|
804
875
|
{pad2(index + 1)}
|
|
805
876
|
</span>
|
|
877
|
+
{commentCount > 0 && <CommentCountBadge count={commentCount} />}
|
|
806
878
|
{(hasTransition || hasSteps) && (
|
|
807
879
|
<div className="flex flex-col items-end gap-0.5">
|
|
808
880
|
{hasTransition && (
|
|
@@ -948,6 +1020,7 @@ function SortableThumb({
|
|
|
948
1020
|
active,
|
|
949
1021
|
activeRef,
|
|
950
1022
|
onSelect,
|
|
1023
|
+
onArrowKeyDown,
|
|
951
1024
|
ariaLabel,
|
|
952
1025
|
children,
|
|
953
1026
|
...rest
|
|
@@ -956,6 +1029,7 @@ function SortableThumb({
|
|
|
956
1029
|
active: boolean;
|
|
957
1030
|
activeRef: React.MutableRefObject<HTMLButtonElement | null> | undefined;
|
|
958
1031
|
onSelect: () => void;
|
|
1032
|
+
onArrowKeyDown: React.KeyboardEventHandler<HTMLButtonElement>;
|
|
959
1033
|
ariaLabel: string;
|
|
960
1034
|
children: React.ReactNode;
|
|
961
1035
|
} & Omit<
|
|
@@ -980,6 +1054,7 @@ function SortableThumb({
|
|
|
980
1054
|
ref={setRef}
|
|
981
1055
|
type="button"
|
|
982
1056
|
onClick={onSelect}
|
|
1057
|
+
onKeyDownCapture={isDragging ? undefined : onArrowKeyDown}
|
|
983
1058
|
aria-label={ariaLabel}
|
|
984
1059
|
aria-current={active ? 'page' : undefined}
|
|
985
1060
|
style={{
|
package/src/app/favicon.ico
CHANGED
|
Binary file
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="512" height="512" viewBox="0 0 512 512" fill="none">
|
|
2
|
+
<defs>
|
|
3
|
+
<linearGradient id="tile" x1="256" y1="16" x2="256" y2="496" gradientUnits="userSpaceOnUse">
|
|
4
|
+
<stop offset="0" stop-color="#FFFFFF"/>
|
|
5
|
+
<stop offset="1" stop-color="#EFEBE6"/>
|
|
6
|
+
</linearGradient>
|
|
7
|
+
<linearGradient id="tile-edge" x1="256" y1="16" x2="256" y2="496" gradientUnits="userSpaceOnUse">
|
|
8
|
+
<stop offset="0" stop-color="#FFFFFF"/>
|
|
9
|
+
<stop offset="0.5" stop-color="#FFFFFF" stop-opacity="0"/>
|
|
10
|
+
<stop offset="1" stop-color="#000000" stop-opacity="0.08"/>
|
|
11
|
+
</linearGradient>
|
|
12
|
+
<linearGradient id="slide" x1="256" y1="172" x2="256" y2="340" gradientUnits="userSpaceOnUse">
|
|
13
|
+
<stop offset="0" stop-color="#FF8A5E"/>
|
|
14
|
+
<stop offset="0.45" stop-color="#FE5226"/>
|
|
15
|
+
<stop offset="1" stop-color="#EE4316"/>
|
|
16
|
+
</linearGradient>
|
|
17
|
+
<linearGradient id="slide-edge" x1="256" y1="172" x2="256" y2="340" gradientUnits="userSpaceOnUse">
|
|
18
|
+
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.55"/>
|
|
19
|
+
<stop offset="0.35" stop-color="#FFFFFF" stop-opacity="0"/>
|
|
20
|
+
<stop offset="0.8" stop-color="#B8300C" stop-opacity="0"/>
|
|
21
|
+
<stop offset="1" stop-color="#B8300C" stop-opacity="0.5"/>
|
|
22
|
+
</linearGradient>
|
|
23
|
+
<radialGradient id="slide-sheen" cx="0" cy="0" r="1" gradientUnits="userSpaceOnUse" gradientTransform="translate(196 184) rotate(32) scale(200 110)">
|
|
24
|
+
<stop offset="0" stop-color="#FFFFFF" stop-opacity="0.26"/>
|
|
25
|
+
<stop offset="1" stop-color="#FFFFFF" stop-opacity="0"/>
|
|
26
|
+
</radialGradient>
|
|
27
|
+
<filter id="lift" x="60" y="140" width="392" height="260" filterUnits="userSpaceOnUse" color-interpolation-filters="sRGB">
|
|
28
|
+
<feDropShadow dx="0" dy="18" stdDeviation="16" flood-color="#FE5226" flood-opacity="0.32"/>
|
|
29
|
+
<feDropShadow dx="0" dy="3" stdDeviation="3" flood-color="#8A2306" flood-opacity="0.22"/>
|
|
30
|
+
</filter>
|
|
31
|
+
</defs>
|
|
32
|
+
<rect x="16" y="16" width="480" height="480" rx="112" fill="url(#tile)"/>
|
|
33
|
+
<rect x="17.5" y="17.5" width="477" height="477" rx="110.5" stroke="url(#tile-edge)" stroke-width="3"/>
|
|
34
|
+
<g filter="url(#lift)">
|
|
35
|
+
<rect x="112" y="172" width="288" height="168" rx="34" fill="url(#slide)"/>
|
|
36
|
+
</g>
|
|
37
|
+
<rect x="112" y="172" width="288" height="168" rx="34" fill="url(#slide-sheen)"/>
|
|
38
|
+
<rect x="113.5" y="173.5" width="285" height="165" rx="32.5" stroke="url(#slide-edge)" stroke-width="3"/>
|
|
39
|
+
</svg>
|
package/src/app/index.html
CHANGED
|
@@ -2,7 +2,8 @@
|
|
|
2
2
|
<html lang="en">
|
|
3
3
|
<head>
|
|
4
4
|
<meta charset="UTF-8" />
|
|
5
|
-
<link rel="icon" href="./favicon.ico" />
|
|
5
|
+
<link rel="icon" href="./favicon.ico" sizes="32x32" />
|
|
6
|
+
<link rel="icon" href="./favicon.svg" type="image/svg+xml" />
|
|
6
7
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
7
8
|
<title>LemonRiver</title>
|
|
8
9
|
<style>
|