@hyperframes/studio 0.8.171 → 0.8.173
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/assets/{hyperframes-player-DohJxMIU.js → hyperframes-player-B-wEZiX3.js} +1 -1
- package/dist/assets/{index-BOkm4Ooc.js → index-Bvkb0Zxy.js} +128 -128
- package/dist/assets/{index-DkQKsFxx.css → index-CESvLzz0.css} +1 -1
- package/dist/assets/{index-DfwtrPPt.js → index-DPugLDRK.js} +1 -1
- package/dist/index.html +2 -2
- package/dist/index.js +1520 -1150
- package/dist/styles.css +1 -1
- package/package.json +8 -8
- package/src/components/DesktopGuideCard.tsx +292 -0
- package/src/components/FrameyGlyph.tsx +77 -27
- package/src/components/OpenInDesktopButton.dom.test.tsx +403 -21
- package/src/components/OpenInDesktopButton.tsx +246 -73
- package/src/styles/components.css +161 -1
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hyperframes/studio",
|
|
3
|
-
"version": "0.8.
|
|
3
|
+
"version": "0.8.173",
|
|
4
4
|
"description": "",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -59,12 +59,12 @@
|
|
|
59
59
|
"dockview-react": "8.3.1",
|
|
60
60
|
"gsap": "^3.13.0",
|
|
61
61
|
"mediabunny": "^1.45.3",
|
|
62
|
-
"@hyperframes/core": "0.8.
|
|
63
|
-
"@hyperframes/
|
|
64
|
-
"@hyperframes/
|
|
65
|
-
"@hyperframes/
|
|
66
|
-
"@hyperframes/
|
|
67
|
-
"@hyperframes/
|
|
62
|
+
"@hyperframes/core": "0.8.173",
|
|
63
|
+
"@hyperframes/parsers": "0.8.173",
|
|
64
|
+
"@hyperframes/player": "0.8.173",
|
|
65
|
+
"@hyperframes/sdk": "0.8.173",
|
|
66
|
+
"@hyperframes/shader-transitions": "0.8.173",
|
|
67
|
+
"@hyperframes/studio-server": "0.8.173"
|
|
68
68
|
},
|
|
69
69
|
"devDependencies": {
|
|
70
70
|
"@tailwindcss/vite": "^4.3.3",
|
|
@@ -81,7 +81,7 @@
|
|
|
81
81
|
"vite": "^6.4.2",
|
|
82
82
|
"vitest": "^4.1.11",
|
|
83
83
|
"zustand": "^5.0.0",
|
|
84
|
-
"@hyperframes/producer": "0.8.
|
|
84
|
+
"@hyperframes/producer": "0.8.173"
|
|
85
85
|
},
|
|
86
86
|
"peerDependencies": {
|
|
87
87
|
"react": "19",
|
|
@@ -0,0 +1,292 @@
|
|
|
1
|
+
import { ArrowSquareOut, Check, Copy } from "@phosphor-icons/react";
|
|
2
|
+
import { useEffect, useState, type ReactNode } from "react";
|
|
3
|
+
import { copyTextToClipboard } from "../utils/clipboard";
|
|
4
|
+
import { FrameyGlyph, type FrameyMood } from "./FrameyGlyph";
|
|
5
|
+
import { Button, cn } from "./ui";
|
|
6
|
+
|
|
7
|
+
// The desktop-app guide card under OpenInDesktopButton: Framey and three steps (download, install, open), all drawn
|
|
8
|
+
// from one `Progress`.
|
|
9
|
+
|
|
10
|
+
export type DesktopRoute = {
|
|
11
|
+
handoff: boolean;
|
|
12
|
+
downloadUrl: string | null;
|
|
13
|
+
context: { os: string; app_installed: boolean; download_available: boolean };
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** The OS for the download, and what "this …" calls the machine. */
|
|
17
|
+
const platformName = (os: string): string =>
|
|
18
|
+
os === "darwin" ? "Mac" : os === "linux" ? "Linux" : "your computer";
|
|
19
|
+
const deviceName = (os: string): string => (os === "darwin" ? "Mac" : "computer");
|
|
20
|
+
|
|
21
|
+
const NOTE = "mt-0.5 text-step-12 text-text-2 text-pretty";
|
|
22
|
+
|
|
23
|
+
/** Re-keys on `value`, so text that changes settles in rather than snapping. */
|
|
24
|
+
export function Swap({ value, children }: { value: string; children: ReactNode }) {
|
|
25
|
+
return (
|
|
26
|
+
<span key={value} className="hf-guide-swap inline-block">
|
|
27
|
+
{children}
|
|
28
|
+
</span>
|
|
29
|
+
);
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
type StepState = "todo" | "now" | "done" | "bad";
|
|
33
|
+
const TODO: StepState = "todo";
|
|
34
|
+
const NOW: StepState = "now";
|
|
35
|
+
const DONE: StepState = "done";
|
|
36
|
+
const BAD: StepState = "bad";
|
|
37
|
+
|
|
38
|
+
function Step({
|
|
39
|
+
n,
|
|
40
|
+
state,
|
|
41
|
+
title,
|
|
42
|
+
children,
|
|
43
|
+
}: {
|
|
44
|
+
n: number;
|
|
45
|
+
state: StepState;
|
|
46
|
+
title: string;
|
|
47
|
+
children: ReactNode;
|
|
48
|
+
}) {
|
|
49
|
+
return (
|
|
50
|
+
<li
|
|
51
|
+
className="grid grid-cols-[22px_minmax(0,1fr)] gap-2.5 border-border py-2 not-first:border-t"
|
|
52
|
+
data-step={n}
|
|
53
|
+
data-state={state}
|
|
54
|
+
>
|
|
55
|
+
<span
|
|
56
|
+
className={cn(
|
|
57
|
+
"grid size-[22px] place-items-center rounded-full text-step-11 font-bold tabular-nums",
|
|
58
|
+
state === "now" && "bg-accent text-on-accent shadow-[0_0_0_3px_var(--color-on)]",
|
|
59
|
+
state === "done" && "bg-on text-accent-ink",
|
|
60
|
+
state === "bad" && "bg-danger/15 text-danger-ink",
|
|
61
|
+
state === "todo" && "bg-hover text-text-2",
|
|
62
|
+
)}
|
|
63
|
+
>
|
|
64
|
+
{state === "done" ? <Check size={12} weight="bold" className="hf-guide-tick" /> : n}
|
|
65
|
+
</span>
|
|
66
|
+
<div className="min-w-0">
|
|
67
|
+
<b
|
|
68
|
+
title={title}
|
|
69
|
+
className={cn(
|
|
70
|
+
"block truncate text-step-13 font-semibold",
|
|
71
|
+
state === "todo" ? "text-text-muted" : "text-text-0",
|
|
72
|
+
)}
|
|
73
|
+
>
|
|
74
|
+
{title}
|
|
75
|
+
</b>
|
|
76
|
+
{children}
|
|
77
|
+
</div>
|
|
78
|
+
</li>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function Spinner() {
|
|
83
|
+
return (
|
|
84
|
+
<span
|
|
85
|
+
aria-hidden="true"
|
|
86
|
+
className="size-3 shrink-0 animate-spin rounded-full border-2 border-on-hover border-t-accent motion-reduce:animate-none"
|
|
87
|
+
/>
|
|
88
|
+
);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/** The project folder, with a copy control that confirms for a moment. */
|
|
92
|
+
function FolderPath({ dir }: { dir: string }) {
|
|
93
|
+
const [copied, setCopied] = useState(false);
|
|
94
|
+
useEffect(() => {
|
|
95
|
+
if (!copied) return;
|
|
96
|
+
const timer = setTimeout(() => setCopied(false), 1500);
|
|
97
|
+
return () => clearTimeout(timer);
|
|
98
|
+
}, [copied]);
|
|
99
|
+
return (
|
|
100
|
+
<div className="mt-1.5 flex min-w-0 items-center gap-1.5">
|
|
101
|
+
<span
|
|
102
|
+
title={dir}
|
|
103
|
+
data-testid="desktop-guide-folder"
|
|
104
|
+
className="min-w-0 flex-1 truncate rounded-md border border-border bg-bg-0 px-2 py-1.5 font-mono text-step-11 text-text-0"
|
|
105
|
+
>
|
|
106
|
+
{dir}
|
|
107
|
+
</span>
|
|
108
|
+
<Button
|
|
109
|
+
variant="secondary"
|
|
110
|
+
aria-label={copied ? "Copied" : "Copy folder path"}
|
|
111
|
+
className="w-ctl px-0"
|
|
112
|
+
onClick={() => void copyTextToClipboard(dir).then((ok) => ok && setCopied(true))}
|
|
113
|
+
>
|
|
114
|
+
{copied ? <Check size={14} className="hf-guide-tick" /> : <Copy size={14} />}
|
|
115
|
+
</Button>
|
|
116
|
+
</div>
|
|
117
|
+
);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Where the person is: what the button says, how Framey looks, and each step's state all derive from this. */
|
|
121
|
+
export type Progress = {
|
|
122
|
+
route: DesktopRoute;
|
|
123
|
+
opening: boolean;
|
|
124
|
+
failed: boolean;
|
|
125
|
+
downloaded: boolean;
|
|
126
|
+
slow: boolean;
|
|
127
|
+
/** The app turned up on this page (not already there): Framey hops once. */
|
|
128
|
+
arrived: boolean;
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
const isInstalled = (p: Progress) => p.route.context.app_installed;
|
|
132
|
+
export const isWaiting = (p: Progress) => p.downloaded && !isInstalled(p);
|
|
133
|
+
|
|
134
|
+
export function moodOf(p: Progress): FrameyMood {
|
|
135
|
+
if (p.opening) return "opening";
|
|
136
|
+
if (p.failed) return "error";
|
|
137
|
+
if (isWaiting(p)) return "thinking";
|
|
138
|
+
return p.arrived ? "found" : "idle";
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
export function labelOf(p: Progress): string {
|
|
142
|
+
if (p.opening) return "Opening in the app…";
|
|
143
|
+
if (p.failed) return "Couldn't open";
|
|
144
|
+
if (p.route.handoff) return "Open in desktop app";
|
|
145
|
+
return isWaiting(p) ? "Finish installing" : "Install the desktop app";
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function subtitleOf(p: Progress): string {
|
|
149
|
+
if (p.failed) return "Almost there: the last step needs another try.";
|
|
150
|
+
if (!isInstalled(p))
|
|
151
|
+
return "Watch it, click a frame, tell Framey what to change. Three steps, about two minutes.";
|
|
152
|
+
return p.route.handoff
|
|
153
|
+
? "Everything is ready. One press and you're in."
|
|
154
|
+
: "Almost there: one last step.";
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
type CardProps = {
|
|
158
|
+
progress: Progress;
|
|
159
|
+
projectDir: string | null;
|
|
160
|
+
onDownload: () => void;
|
|
161
|
+
onOpen: () => void;
|
|
162
|
+
};
|
|
163
|
+
|
|
164
|
+
function DownloadStep({ progress, onDownload }: CardProps) {
|
|
165
|
+
const { context, downloadUrl } = progress.route;
|
|
166
|
+
const where = platformName(context.os);
|
|
167
|
+
const installed = isInstalled(progress);
|
|
168
|
+
if (!progress.downloaded && !installed)
|
|
169
|
+
return (
|
|
170
|
+
<Step n={1} state={NOW} title={`Download HyperFrames for ${where}`}>
|
|
171
|
+
<p className={NOTE}>Free. Opens hyperframes.dev in a new tab.</p>
|
|
172
|
+
<Button
|
|
173
|
+
variant="primary"
|
|
174
|
+
className="mt-2"
|
|
175
|
+
icon={<ArrowSquareOut size={14} />}
|
|
176
|
+
onClick={onDownload}
|
|
177
|
+
>
|
|
178
|
+
Download for {where}
|
|
179
|
+
</Button>
|
|
180
|
+
</Step>
|
|
181
|
+
);
|
|
182
|
+
return (
|
|
183
|
+
<Step n={1} state={DONE} title={`Download HyperFrames for ${where}`}>
|
|
184
|
+
<p className={NOTE}>
|
|
185
|
+
{installed ? `Already on this ${deviceName(context.os)}.` : "Started in your browser."}
|
|
186
|
+
{!installed && downloadUrl && (
|
|
187
|
+
<button
|
|
188
|
+
type="button"
|
|
189
|
+
className="ml-1.5 cursor-pointer font-medium text-accent-ink hover:underline"
|
|
190
|
+
onClick={onDownload}
|
|
191
|
+
>
|
|
192
|
+
Download again
|
|
193
|
+
</button>
|
|
194
|
+
)}
|
|
195
|
+
</p>
|
|
196
|
+
</Step>
|
|
197
|
+
);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function InstallStep({ progress }: CardProps) {
|
|
201
|
+
const { os } = progress.route.context;
|
|
202
|
+
if (isInstalled(progress))
|
|
203
|
+
return (
|
|
204
|
+
<Step n={2} state={DONE} title="Install it">
|
|
205
|
+
<p className={NOTE}>
|
|
206
|
+
<Swap value="found">Found HyperFrames on this {deviceName(os)}.</Swap>
|
|
207
|
+
</p>
|
|
208
|
+
</Step>
|
|
209
|
+
);
|
|
210
|
+
return (
|
|
211
|
+
<Step n={2} state={progress.downloaded ? NOW : TODO} title="Install it">
|
|
212
|
+
<p className={NOTE}>
|
|
213
|
+
{os === "darwin"
|
|
214
|
+
? "Open the download and drag HyperFrames into Applications."
|
|
215
|
+
: "Open the download to install HyperFrames."}
|
|
216
|
+
</p>
|
|
217
|
+
{progress.downloaded && (
|
|
218
|
+
<p className="mt-1.5 flex items-center gap-2 text-step-12 text-text-2" role="status">
|
|
219
|
+
<Spinner />
|
|
220
|
+
<Swap value={String(progress.slow)}>
|
|
221
|
+
{progress.slow ? "Still looking. Did the download finish?" : "Looking for the app…"}
|
|
222
|
+
</Swap>
|
|
223
|
+
</p>
|
|
224
|
+
)}
|
|
225
|
+
</Step>
|
|
226
|
+
);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
function OpenStep({ progress, projectDir, onOpen }: CardProps) {
|
|
230
|
+
const title = `Open ${projectDir?.split(/[\\/]/).filter(Boolean).pop() ?? "this project"}`;
|
|
231
|
+
const folder = projectDir && <FolderPath dir={projectDir} />;
|
|
232
|
+
if (progress.failed)
|
|
233
|
+
return (
|
|
234
|
+
<Step n={3} state={BAD} title={title}>
|
|
235
|
+
<p className={NOTE}>
|
|
236
|
+
The app didn't start. Try again, or open HyperFrames and drop this folder in:
|
|
237
|
+
</p>
|
|
238
|
+
{folder}
|
|
239
|
+
<Button variant="primary" className="mt-2" loading={progress.opening} onClick={onOpen}>
|
|
240
|
+
Try again
|
|
241
|
+
</Button>
|
|
242
|
+
</Step>
|
|
243
|
+
);
|
|
244
|
+
const installed = isInstalled(progress);
|
|
245
|
+
// Installed, but this platform's app cannot take a handed-over project yet: the folder is the way in.
|
|
246
|
+
if (installed && !progress.route.handoff)
|
|
247
|
+
return (
|
|
248
|
+
<Step n={3} state={NOW} title={title}>
|
|
249
|
+
<p className={NOTE}>Open HyperFrames and drop this folder in:</p>
|
|
250
|
+
{folder}
|
|
251
|
+
</Step>
|
|
252
|
+
);
|
|
253
|
+
return (
|
|
254
|
+
<Step n={3} state={installed ? NOW : TODO} title={title}>
|
|
255
|
+
<p className={NOTE}>Same project, same files, in the desktop app.</p>
|
|
256
|
+
<Button
|
|
257
|
+
variant="primary"
|
|
258
|
+
className="mt-2"
|
|
259
|
+
disabled={!progress.route.handoff}
|
|
260
|
+
loading={progress.opening}
|
|
261
|
+
onClick={onOpen}
|
|
262
|
+
>
|
|
263
|
+
Open in desktop app
|
|
264
|
+
</Button>
|
|
265
|
+
</Step>
|
|
266
|
+
);
|
|
267
|
+
}
|
|
268
|
+
|
|
269
|
+
export function GuideCard(props: CardProps) {
|
|
270
|
+
const { progress } = props;
|
|
271
|
+
const subtitle = subtitleOf(progress);
|
|
272
|
+
return (
|
|
273
|
+
<div data-testid="desktop-guide">
|
|
274
|
+
<div className="grid grid-cols-[auto_minmax(0,1fr)] items-center gap-3 bg-linear-to-b from-accent/8 to-transparent px-4 pt-4 pb-3">
|
|
275
|
+
<FrameyGlyph size={46} mood={moodOf(progress)} />
|
|
276
|
+
<div className="min-w-0">
|
|
277
|
+
<p className="text-step-14 font-semibold text-balance text-text-0">
|
|
278
|
+
Edit this video in the desktop app
|
|
279
|
+
</p>
|
|
280
|
+
<p className={NOTE}>
|
|
281
|
+
<Swap value={subtitle}>{subtitle}</Swap>
|
|
282
|
+
</p>
|
|
283
|
+
</div>
|
|
284
|
+
</div>
|
|
285
|
+
<ol className="m-0 grid list-none px-4 pt-1 pb-4">
|
|
286
|
+
<DownloadStep {...props} />
|
|
287
|
+
<InstallStep {...props} />
|
|
288
|
+
<OpenStep {...props} />
|
|
289
|
+
</ol>
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
@@ -1,51 +1,101 @@
|
|
|
1
1
|
import type { Ref } from "react";
|
|
2
|
+
import { cn } from "./ui";
|
|
2
3
|
|
|
3
|
-
// Framey, the desktop app's cursor mascot, as
|
|
4
|
-
//
|
|
4
|
+
// Framey, the desktop app's cursor mascot, as the app draws it: a rounded triangle with a white rim and one eye,
|
|
5
|
+
// framed square around the body and rim.
|
|
5
6
|
const BODY = "M77.55 48.44 L122.77 65.85 L99.3 89.41 Z";
|
|
6
7
|
const EYE = { cx: 111.6, cy: 68.2, r: 11.5 };
|
|
8
|
+
const RIM = 24;
|
|
9
|
+
const VIEW_BOX = "65.55 34.315 69.22 69.22";
|
|
7
10
|
|
|
8
|
-
/**
|
|
11
|
+
/** The app's moods: what Framey's eye and body do (styles in components.css, `.hf-framey-mood`). */
|
|
12
|
+
export type FrameyMood = "idle" | "thinking" | "found" | "opening" | "error";
|
|
13
|
+
|
|
14
|
+
const INK = "var(--color-framey-ink)";
|
|
15
|
+
|
|
16
|
+
function Pupils({ mood }: { mood?: FrameyMood }) {
|
|
17
|
+
const { cx, cy } = EYE;
|
|
18
|
+
if (mood === "found")
|
|
19
|
+
return (
|
|
20
|
+
<g transform={`translate(${cx + 1.7} ${cy - 2.3})`}>
|
|
21
|
+
<circle r={4.6} fill={INK} />
|
|
22
|
+
<circle cx={1.4} cy={-1.6} r={1.3} fill="var(--color-white)" />
|
|
23
|
+
</g>
|
|
24
|
+
);
|
|
25
|
+
if (mood === "thinking")
|
|
26
|
+
return (
|
|
27
|
+
<g transform={`translate(${cx} ${cy})`}>
|
|
28
|
+
<g className="hf-framey-orbit">
|
|
29
|
+
<circle cx={-2.8} r={1.9} fill={INK} />
|
|
30
|
+
<circle cx={2.8} r={1.9} fill={INK} />
|
|
31
|
+
</g>
|
|
32
|
+
</g>
|
|
33
|
+
);
|
|
34
|
+
if (mood === "error")
|
|
35
|
+
return (
|
|
36
|
+
<path
|
|
37
|
+
transform={`translate(${cx} ${cy})`}
|
|
38
|
+
d="M-5 -2 L-1.4 1.6 M-1.4 -2 L-5 1.6 M1.4 -2 L5 1.6 M5 -2 L1.4 1.6"
|
|
39
|
+
stroke={INK}
|
|
40
|
+
strokeWidth={1.7}
|
|
41
|
+
strokeLinecap="round"
|
|
42
|
+
/>
|
|
43
|
+
);
|
|
44
|
+
return (
|
|
45
|
+
<path
|
|
46
|
+
d={`M${cx - 2.6} ${cy - 2.4} V${cy + 2.4} M${cx + 2.6} ${cy - 2.4} V${cy + 2.4}`}
|
|
47
|
+
stroke={INK}
|
|
48
|
+
strokeWidth={2.4}
|
|
49
|
+
strokeLinecap="round"
|
|
50
|
+
/>
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/** `eyeRef` moves the pupils (a translate of a few px) without re-rendering. Without `mood` Framey holds still. */
|
|
9
55
|
export function FrameyGlyph({
|
|
10
56
|
size,
|
|
11
57
|
className,
|
|
12
58
|
eyeRef,
|
|
59
|
+
mood,
|
|
13
60
|
}: {
|
|
14
61
|
size: number;
|
|
15
62
|
className?: string;
|
|
16
63
|
eyeRef?: Ref<SVGGElement>;
|
|
64
|
+
mood?: FrameyMood;
|
|
17
65
|
}) {
|
|
18
66
|
return (
|
|
19
67
|
<svg
|
|
20
68
|
width={size}
|
|
21
69
|
height={size}
|
|
22
|
-
viewBox=
|
|
70
|
+
viewBox={VIEW_BOX}
|
|
23
71
|
aria-hidden="true"
|
|
24
|
-
className={className}
|
|
25
72
|
overflow="visible"
|
|
73
|
+
className={cn(mood && "hf-framey-mood", className)}
|
|
74
|
+
data-mood={mood}
|
|
26
75
|
>
|
|
27
|
-
<
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
76
|
+
<g className="hf-framey-whole">
|
|
77
|
+
<g className="hf-framey-body">
|
|
78
|
+
<path
|
|
79
|
+
d={BODY}
|
|
80
|
+
fill="var(--color-white)"
|
|
81
|
+
stroke="var(--color-white)"
|
|
82
|
+
strokeWidth={RIM}
|
|
83
|
+
strokeLinejoin="round"
|
|
84
|
+
/>
|
|
85
|
+
<path
|
|
86
|
+
d={BODY}
|
|
87
|
+
fill="var(--color-framey)"
|
|
88
|
+
stroke="var(--color-framey)"
|
|
89
|
+
strokeWidth={19}
|
|
90
|
+
strokeLinejoin="round"
|
|
91
|
+
/>
|
|
92
|
+
<g className="hf-framey-lid">
|
|
93
|
+
<circle cx={EYE.cx} cy={EYE.cy} r={EYE.r} fill="var(--color-white)" />
|
|
94
|
+
<g ref={eyeRef} className="hf-framey-eye">
|
|
95
|
+
<Pupils mood={mood} />
|
|
96
|
+
</g>
|
|
97
|
+
</g>
|
|
98
|
+
</g>
|
|
49
99
|
</g>
|
|
50
100
|
</svg>
|
|
51
101
|
);
|