@lemonriver/core 0.1.0 → 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.
Files changed (83) hide show
  1. package/LICENSE +21 -0
  2. package/dist/{build-f-fM2ZY4.js → build-Df_xwIpg.js} +1 -1
  3. package/dist/cli/bin.js +4 -4
  4. package/dist/{config-DO45GU3e.js → config-BjWCLoMQ.js} +2343 -363
  5. package/dist/{config-B6JaKJM4.d.ts → config-rMGCkz6i.d.ts} +6 -2
  6. package/dist/{dev-CqS2xpgE.js → dev-Ctsx9IeO.js} +2 -2
  7. package/dist/index.d.ts +3 -3
  8. package/dist/index.js +4 -3
  9. package/dist/locale/index.d.ts +5 -2
  10. package/dist/locale/index.js +2 -2
  11. package/dist/{preview-BihgMlEl.js → preview-CJgcqsPk.js} +1 -1
  12. package/dist/{types-BjW4glip.d.ts → types-8VUyA2bu.d.ts} +133 -5
  13. package/dist/{update-C_R0ycVC.js → update-BKOA3Z6o.js} +153 -21
  14. package/dist/vite/index.d.ts +1 -1
  15. package/dist/vite/index.js +1 -1
  16. package/dist/{zh-tw-Ci0jK8eg.js → zh-tw-CeLrYwRE.js} +1178 -25
  17. package/package.json +21 -15
  18. package/skills/apply-comments/SKILL.md +10 -6
  19. package/skills/create-slide/SKILL.md +2 -0
  20. package/skills/current-slide/SKILL.md +1 -0
  21. package/skills/slide-authoring/SKILL.md +31 -1
  22. package/src/agent/prompt.ts +141 -0
  23. package/src/agent/provider.ts +123 -0
  24. package/src/agent/types.ts +151 -0
  25. package/src/app/app.tsx +2 -0
  26. package/src/app/assets/open-slide.svg +39 -0
  27. package/src/app/components/agent/agent-dock.tsx +35 -0
  28. package/src/app/components/agent/agent-markdown.tsx +45 -0
  29. package/src/app/components/agent/agent-panel.tsx +692 -0
  30. package/src/app/components/agent/agent-presence.tsx +74 -0
  31. package/src/app/components/agent/composer.tsx +120 -0
  32. package/src/app/components/agent/connect-view.tsx +184 -0
  33. package/src/app/components/agent/history-view.tsx +135 -0
  34. package/src/app/components/agent/provider-logo.tsx +34 -0
  35. package/src/app/components/agent/transcript.tsx +609 -0
  36. package/src/app/components/board/board-canvas.tsx +143 -12
  37. package/src/app/components/board/board.css +51 -0
  38. package/src/app/components/board/layout.test.ts +53 -1
  39. package/src/app/components/board/layout.ts +50 -2
  40. package/src/app/components/board/page-node.tsx +287 -13
  41. package/src/app/components/board/use-board-cards.ts +210 -0
  42. package/src/app/components/board/use-page-order.ts +17 -9
  43. package/src/app/components/command/slide-command-menu.tsx +10 -0
  44. package/src/app/components/icon-switcher-indicator.tsx +1 -1
  45. package/src/app/components/inspector/comment-layer.tsx +1011 -0
  46. package/src/app/components/inspector/image-crop-dialog.tsx +1 -5
  47. package/src/app/components/inspector/inspect-overlay.tsx +2 -39
  48. package/src/app/components/inspector/inspector-panel.tsx +10 -93
  49. package/src/app/components/inspector/inspector-provider.tsx +121 -47
  50. package/src/app/components/panel/editor-sidebar.tsx +85 -16
  51. package/src/app/components/panel/panel-shell.tsx +2 -2
  52. package/src/app/components/panel/resize-handle.tsx +129 -0
  53. package/src/app/components/player.tsx +0 -1
  54. package/src/app/components/preview-step-host.tsx +16 -0
  55. package/src/app/components/sidebar/sidebar.tsx +2 -2
  56. package/src/app/components/style-panel/style-panel.tsx +3 -2
  57. package/src/app/components/thumbnail-rail.tsx +76 -1
  58. package/src/app/favicon.ico +0 -0
  59. package/src/app/favicon.svg +39 -0
  60. package/src/app/index.html +2 -1
  61. package/src/app/lib/agent/use-agent.ts +543 -0
  62. package/src/app/lib/inspector/freeze-motion.ts +61 -0
  63. package/src/app/lib/inspector/use-comments.ts +32 -3
  64. package/src/app/lib/locale-store.ts +6 -1
  65. package/src/app/lib/print-ready.ts +9 -6
  66. package/src/app/lib/use-auto-grow.ts +15 -0
  67. package/src/app/lib/utils.ts +4 -0
  68. package/src/app/routes/board.tsx +48 -11
  69. package/src/app/routes/home.tsx +43 -7
  70. package/src/app/routes/presenter.tsx +1 -16
  71. package/src/app/routes/preview.tsx +169 -0
  72. package/src/app/routes/slide.tsx +221 -173
  73. package/src/app/styles.css +234 -80
  74. package/src/editing/placeholder-id.ts +1 -0
  75. package/src/locale/en.ts +127 -5
  76. package/src/locale/es.ts +692 -0
  77. package/src/locale/index.ts +1 -0
  78. package/src/locale/ja.ts +120 -6
  79. package/src/locale/types.ts +132 -5
  80. package/src/locale/zh-cn.ts +117 -5
  81. package/src/locale/zh-tw.ts +117 -5
  82. package/src/app/assets/open-slide.png +0 -0
  83. 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.png';
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 rounded-[6px] ring-1 ring-foreground/10"
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 md:inline">{t.stylePanel.designToggle}</span>
205
- <kbd className="ml-1 hidden rounded-[3px] bg-foreground/10 px-1 font-mono text-[9.5px] tracking-[0.04em] md:inline">
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={{
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>
@@ -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>