a collection of tools for fly for fun universe skillulator.lol

anchor fixes

besaid.zone ab4912a1 2d276988

verified
Changed files
+171 -167
apps
skillulator
+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
··· 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
··· 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
··· 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">
+1 -1
apps/skillulator/src/routes/c/$class/components/action-buttons.tsx
··· 89 89 const popoverRef = useRef<ComponentRef<"div">>(null); 90 90 91 91 return ( 92 - <div style={{ anchorName: "--attributes" }}> 92 + <div style={{ anchorName: "--attributes-popover" }}> 93 93 {/* biome-ignore lint/a11y/useKeyWithMouseEvents: <explanation> */} 94 94 <button 95 95 type="button"
+1 -1
package.json
··· 15 15 ], 16 16 "author": "Dane Miller (me@dane.computer)", 17 17 "license": "MIT", 18 - "packageManager": "pnpm@10.15.0", 18 + "packageManager": "pnpm@10.25.0", 19 19 "engines": { 20 20 "node": ">=22" 21 21 },