+5
-5
apps/skillulator/index.html
+5
-5
apps/skillulator/index.html
···
1
1
<!doctype html>
2
-
<html lang="en" dir="ltr" class="has-[dialog[open]]:overflow-hidden">
2
+
<html lang="en" dir="ltr" class="has-[dialog[open]]:overflow-hidden bg-gray-50 h-full">
3
3
<head>
4
4
<meta charset="UTF-8" />
5
5
<link rel="icon" type="image/svg+xml" href="/src/favicon.svg" />
···
7
7
<title>Skillulator | FlyFF Universe Skill Calculator</title>
8
8
<meta
9
9
name="description"
10
-
content="Skillulator helps you optimize and share your FlyFF skill builds"
10
+
content="Skill tree calculator for Fly for Fun Universe"
11
11
/>
12
12
<meta property="og:type" content="website" />
13
13
<meta
···
16
16
/>
17
17
<meta
18
18
property="og:description"
19
-
content="Skillulator helps you optimize and share your FlyFF skill builds"
19
+
content="Skill tree calculator for Fly for Fun Universe"
20
20
/>
21
21
<meta property="og:url" content="https://skillulator.lol" />
22
22
<link rel="canonical" href="https://skillulator.lol" />
23
23
</head>
24
-
<body>
25
-
<div id="root"></div>
24
+
<body class="h-full">
25
+
<div id="root" class="h-full flex flex-col"></div>
26
26
<script type="module" src="/src/main.tsx"></script>
27
27
</body>
28
28
</html>
+28
-27
apps/skillulator/src/index.css
+28
-27
apps/skillulator/src/index.css
···
8
8
@import url("./css/jester.css");
9
9
@import "tailwindcss";
10
10
11
-
@layer base {
11
+
.a11y-focus {
12
+
@apply focus:border-transparent focus:outline-transparent focus:ring-4 focus:ring-offset-2 focus:ring-indigo-500;
13
+
}
12
14
13
-
html,
14
-
body {
15
-
@apply bg-gray-50;
16
-
height: 100%;
17
-
}
15
+
.skill-level {
16
+
position: absolute;
17
+
bottom: 1px;
18
+
right: 5px;
19
+
font-size: 16px;
20
+
font-weight: bold;
21
+
color: white;
22
+
text-shadow: -1px -1px 0 #f00, 1px -1px 0 #f00, -1px 1px 0 #f00, 1px 1px 0
23
+
#f00;
24
+
}
18
25
19
-
.a11y-focus {
20
-
@apply focus:border-transparent focus:outline-transparent focus:ring-4 focus:ring-offset-2 focus:ring-indigo-500;
21
-
}
26
+
.scoped-anchor {
27
+
anchor-scope: --attributes-popover;
28
+
}
22
29
23
-
.skill-level {
24
-
position: absolute;
25
-
bottom: 1px;
26
-
right: 5px;
27
-
font-size: 16px;
28
-
font-weight: bold;
29
-
color: white;
30
-
text-shadow: -1px -1px 0 #f00, 1px -1px 0 #f00, -1px 1px 0 #f00, 1px 1px 0 #f00;
31
-
}
30
+
.attributes-popover {
31
+
inset: auto;
32
+
margin: 0;
33
+
position-anchor: --attributes-popover;
34
+
position-area: bottom right;
35
+
position-try-fallbacks: --top-center, --top-left;
36
+
}
32
37
33
-
#attributesPopover {
34
-
position-anchor: --attributes;
35
-
top: anchor(bottom);
36
-
right: anchor(right);
37
-
}
38
+
@position-try --top-left {
39
+
position-area: top left;
40
+
}
38
41
39
-
#root {
40
-
height: 100%;
41
-
@apply flex flex-col;
42
-
}
42
+
@position-try --top-center {
43
+
position-area: center span-left;
43
44
}
+135
-131
apps/skillulator/src/routes/c/$class/components/Skill.tsx
+135
-131
apps/skillulator/src/routes/c/$class/components/Skill.tsx
···
2
2
import { languages } from "@/utils/constants";
3
3
import { getLanguageForSkill } from "@/utils/language";
4
4
import {
5
-
// DecreaseSkillPointButton,
6
-
IncreaseSkillToMaxButton,
7
-
SkillIconButton,
5
+
// DecreaseSkillPointButton,
6
+
IncreaseSkillToMaxButton,
7
+
SkillIconButton,
8
8
} from "./action-buttons";
9
9
import { Requirement } from "./Requirement";
10
10
import { useRef, type ComponentRef, type RefObject } from "react";
11
11
12
12
interface SkillProps {
13
-
skill: SkillType;
14
-
jobId: number | undefined;
15
-
skillId: number;
16
-
hasMinLevelRequirements: boolean;
17
-
isMaxed: boolean;
18
-
lang: string;
13
+
skill: SkillType;
14
+
jobId: number | undefined;
15
+
skillId: number;
16
+
hasMinLevelRequirements: boolean;
17
+
isMaxed: boolean;
18
+
lang: string;
19
19
}
20
20
21
21
export default function Skill(props: SkillProps) {
22
-
const locale = getLanguageForSkill(languages, props.lang);
23
-
const skillName = props.skill.name.en
24
-
.replaceAll(" ", "")
25
-
.replace(/'/, "")
26
-
.replace(/[\[\]()]/g, "");
22
+
const locale = getLanguageForSkill(languages, props.lang);
23
+
const skillName = props.skill.name.en
24
+
.replaceAll(" ", "")
25
+
.replace(/'/, "")
26
+
.replace(/[\[\]()]/g, "");
27
27
28
-
const isMasterVariationSkill =
29
-
typeof props?.skill?.masterVariations?.length !== "undefined";
28
+
const isMasterVariationSkill =
29
+
typeof props?.skill?.masterVariations?.length !== "undefined";
30
30
31
-
const masterDialogRef = useRef<ComponentRef<"dialog">>(null);
31
+
const masterDialogRef = useRef<ComponentRef<"dialog">>(null);
32
32
33
-
return (
34
-
<div
35
-
data-skill={skillName}
36
-
className="relative flex flex-col items-center flex-1 pt-3 bg-white border border-gray-300 rounded-md basis-1/2 min-content"
37
-
>
38
-
<SkillIconButton {...props} locale={locale} />
39
-
{props.skill.isPassiveSkill ? <span className="text-sm font-semibold text-purple-600">(passive skill)</span> : null}
40
-
<div>
41
-
{props.skill.requirements.map((skill, index: number) => (
42
-
<Requirement
43
-
key={JSON.stringify({ skill, index })}
44
-
hasMinLevelRequirements={skill.hasMinLevel}
45
-
skill={{ level: skill.level, name: skill.name }}
46
-
/>
47
-
))}
48
-
</div>
49
-
<div>
50
-
<button
51
-
type="button"
52
-
aria-label="open master variation dialog"
53
-
onClick={() => masterDialogRef?.current?.showModal()}
54
-
>
55
-
{isMasterVariationSkill ? (
56
-
<img
57
-
src="https://api.flyff.com/image/badge/master6.png"
58
-
alt=""
59
-
className="h-6 w-6 absolute top-2 left-3 cursor-pointer"
60
-
/>
61
-
) : null}
62
-
</button>
63
-
{/* <DecreaseSkillPointButton {...props} /> */}
64
-
<IncreaseSkillToMaxButton {...props} />
65
-
</div>
66
-
<MasterSkillVariationDialog
67
-
ref={masterDialogRef}
68
-
masterVariations={props?.skill?.masterVariations}
69
-
{...props}
70
-
/>
71
-
</div>
72
-
);
33
+
return (
34
+
<div
35
+
data-skill={skillName}
36
+
className="scoped-anchor relative flex flex-col items-center flex-1 pt-3 bg-white border border-gray-300 rounded-md basis-1/2 min-content"
37
+
>
38
+
<SkillIconButton {...props} locale={locale} />
39
+
{props.skill.isPassiveSkill ? (
40
+
<span className="text-sm font-semibold text-purple-600">
41
+
(passive skill)
42
+
</span>
43
+
) : null}
44
+
<div>
45
+
{props.skill.requirements.map((skill, index: number) => (
46
+
<Requirement
47
+
key={JSON.stringify({ skill, index })}
48
+
hasMinLevelRequirements={skill.hasMinLevel}
49
+
skill={{ level: skill.level, name: skill.name }}
50
+
/>
51
+
))}
52
+
</div>
53
+
<div>
54
+
<button
55
+
type="button"
56
+
aria-label="open master variation dialog"
57
+
onClick={() => masterDialogRef?.current?.showModal()}
58
+
>
59
+
{isMasterVariationSkill ? (
60
+
<img
61
+
src="https://api.flyff.com/image/badge/master6.png"
62
+
alt=""
63
+
className="h-6 w-6 absolute top-2 left-3 cursor-pointer"
64
+
/>
65
+
) : null}
66
+
</button>
67
+
{/* <DecreaseSkillPointButton {...props} /> */}
68
+
<IncreaseSkillToMaxButton {...props} />
69
+
</div>
70
+
<MasterSkillVariationDialog
71
+
ref={masterDialogRef}
72
+
masterVariations={props?.skill?.masterVariations}
73
+
{...props}
74
+
/>
75
+
</div>
76
+
);
73
77
}
74
78
75
79
function MasterSkillVariationDialog(props: {
76
-
ref: RefObject<HTMLDialogElement | null>;
77
-
masterVariations: SkillType["masterVariations"];
78
-
skill: SkillType;
79
-
jobId: number | undefined;
80
-
skillId: number;
81
-
hasMinLevelRequirements: boolean;
82
-
isMaxed: boolean;
83
-
lang: string;
80
+
ref: RefObject<HTMLDialogElement | null>;
81
+
masterVariations: SkillType["masterVariations"];
82
+
skill: SkillType;
83
+
jobId: number | undefined;
84
+
skillId: number;
85
+
hasMinLevelRequirements: boolean;
86
+
isMaxed: boolean;
87
+
lang: string;
84
88
}) {
85
-
return (
86
-
<dialog
87
-
ref={props.ref}
88
-
className="border border-gray-300 shadow-sm rounded-md p-5 w-1/2 xl:w-[60%] mx-auto my-auto"
89
-
>
90
-
<div className="flex justify-between mb-4">
91
-
<h2 className="font-bold">
92
-
Select a Master Variation (you can only choose one)
93
-
</h2>
94
-
<button
95
-
type="button"
96
-
onClick={() => props?.ref?.current?.close()}
97
-
className="text-sm bg-red-500 text-white py-1 px-2 rounded-md cursor-pointer"
98
-
>
99
-
Close Dialog
100
-
</button>
101
-
</div>
102
-
<div className="flex gap-4">
103
-
{props?.masterVariations?.map((variation, index) => {
104
-
const selectedMasterVariation = props?.masterVariations?.some(
105
-
(s) => s.isSelected === true,
106
-
)
107
-
? props?.masterVariations?.filter((s) => s.isSelected === true)
108
-
: props.masterVariations;
89
+
return (
90
+
<dialog
91
+
ref={props.ref}
92
+
className="border border-gray-300 shadow-sm rounded-md p-5 w-1/2 xl:w-[60%] mx-auto my-auto"
93
+
>
94
+
<div className="flex justify-between mb-4">
95
+
<h2 className="font-bold">
96
+
Select a Master Variation (you can only choose one)
97
+
</h2>
98
+
<button
99
+
type="button"
100
+
onClick={() => props?.ref?.current?.close()}
101
+
className="text-sm bg-red-500 text-white py-1 px-2 rounded-md cursor-pointer"
102
+
>
103
+
Close Dialog
104
+
</button>
105
+
</div>
106
+
<div className="flex gap-4">
107
+
{props?.masterVariations?.map((variation, index) => {
108
+
const selectedMasterVariation = props?.masterVariations?.some(
109
+
(s) => s.isSelected === true,
110
+
)
111
+
? props?.masterVariations?.filter((s) => s.isSelected === true)
112
+
: props.masterVariations;
109
113
110
-
const isSelectedMasterVariation =
111
-
!selectedMasterVariation?.every((v) => v.id === variation.id) &&
112
-
selectedMasterVariation?.length === 1;
114
+
const isSelectedMasterVariation =
115
+
!selectedMasterVariation?.every((v) => v.id === variation.id) &&
116
+
selectedMasterVariation?.length === 1;
113
117
114
-
return (
115
-
<div
116
-
key={JSON.stringify({ id: variation.id, index })}
117
-
data-skill={props.skill.name}
118
-
className="relative flex flex-col items-center flex-1 py-2 bg-white border border-gray-300 rounded-md basis-1/2 min-content"
119
-
>
120
-
<SkillIconButton
121
-
locale={props.lang}
122
-
skill={{ ...variation, id: props.skill.id }}
123
-
masterVariationSkillId={variation?.id}
124
-
isSelectedMasterVariation={isSelectedMasterVariation}
125
-
jobId={props.jobId}
126
-
hasMinLevelRequirements={variation?.hasMinLevelRequirements}
127
-
isMaxed={variation?.isMaxed}
128
-
/>
129
-
<div>
130
-
{variation.requirements.map((variation, index: number) => (
131
-
<Requirement
132
-
key={JSON.stringify({ variation, index })}
133
-
hasMinLevelRequirements={variation.hasMinLevel}
134
-
skill={{ level: variation.level, name: variation.name }}
135
-
/>
136
-
))}
137
-
</div>
138
-
{/* <DecreaseSkillPointButton {...props} /> */}
139
-
<IncreaseSkillToMaxButton
140
-
skillId={props.skill.id}
141
-
jobId={props.jobId}
142
-
isSelectedMasterVariation={isSelectedMasterVariation}
143
-
masterVariationSkillId={variation.id}
144
-
hasMinLevelRequirements={variation?.hasMinLevelRequirements}
145
-
isMaxed={variation?.isMaxed}
146
-
/>
147
-
</div>
148
-
);
149
-
})}
150
-
</div>
151
-
</dialog>
152
-
);
118
+
return (
119
+
<div
120
+
key={JSON.stringify({ id: variation.id, index })}
121
+
data-skill={props.skill.name}
122
+
className="relative flex flex-col items-center flex-1 py-2 bg-white border border-gray-300 rounded-md basis-1/2 min-content"
123
+
>
124
+
<SkillIconButton
125
+
locale={props.lang}
126
+
skill={{ ...variation, id: props.skill.id }}
127
+
masterVariationSkillId={variation?.id}
128
+
isSelectedMasterVariation={isSelectedMasterVariation}
129
+
jobId={props.jobId}
130
+
hasMinLevelRequirements={variation?.hasMinLevelRequirements}
131
+
isMaxed={variation?.isMaxed}
132
+
/>
133
+
<div>
134
+
{variation.requirements.map((variation, index: number) => (
135
+
<Requirement
136
+
key={JSON.stringify({ variation, index })}
137
+
hasMinLevelRequirements={variation.hasMinLevel}
138
+
skill={{ level: variation.level, name: variation.name }}
139
+
/>
140
+
))}
141
+
</div>
142
+
{/* <DecreaseSkillPointButton {...props} /> */}
143
+
<IncreaseSkillToMaxButton
144
+
skillId={props.skill.id}
145
+
jobId={props.jobId}
146
+
isSelectedMasterVariation={isSelectedMasterVariation}
147
+
masterVariationSkillId={variation.id}
148
+
hasMinLevelRequirements={variation?.hasMinLevelRequirements}
149
+
isMaxed={variation?.isMaxed}
150
+
/>
151
+
</div>
152
+
);
153
+
})}
154
+
</div>
155
+
</dialog>
156
+
);
153
157
}
+1
-2
apps/skillulator/src/routes/c/$class/components/Tooltip.tsx
+1
-2
apps/skillulator/src/routes/c/$class/components/Tooltip.tsx
···
271
271
export function Tooltip(props: TooltipProps) {
272
272
return (
273
273
<div
274
-
id="attributesPopover"
275
274
popover="auto"
276
275
ref={props.popoverRef}
277
-
className="absolute m-0 inset-auto h-min w-[400px] shadow-sm border-2 border-indigo-700 rounded-sm p-4 bg-black/80"
276
+
className="absolute shadow-sm border-2 border-indigo-700 rounded-sm p-4 bg-black/80 text-left attributes-popover w-[350px]"
278
277
>
279
278
<div className="flex gap-1 text-white">
280
279
<h2 className="font-bold text-green-500">